Add project files.
This commit is contained in:
370
Nodify/Interactivity/DragState.cs
Normal file
370
Nodify/Interactivity/DragState.cs
Normal file
@@ -0,0 +1,370 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an abstract base class for managing drag interactions within a UI element.
|
||||
/// Provides a framework for handling input gestures such as starting, canceling, and completing drag interactions.
|
||||
/// </summary>
|
||||
/// <typeparam name="TElement">The type of <see cref="FrameworkElement"/> that owns the state.</typeparam>
|
||||
public abstract class DragState<TElement> : InputElementState<TElement>, IInputHandler
|
||||
where TElement : FrameworkElement
|
||||
{
|
||||
private enum InteractionState
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that no drag interaction is active. This is the initial state or the state after a drag interaction has been canceled or completed.
|
||||
/// </summary>
|
||||
Ready,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a drag interaction is currently active and handling input events. This state is entered when a drag begins.
|
||||
/// </summary>
|
||||
InProgress,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a drag interaction is in the process of ending. This state is used to handle toggled interactions (see <see cref="IsToggle"/>).
|
||||
/// </summary>
|
||||
Ending
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the gesture used to cancel the drag interaction, if defined.
|
||||
/// </summary>
|
||||
protected InputGesture? CancelGesture { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the gesture used to begin the drag interaction.
|
||||
/// </summary>
|
||||
protected InputGesture BeginGesture { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the element has a context menu associated with it.
|
||||
/// </summary>
|
||||
/// <remarks>This property is used to suppress the context menu when a drag interaction is performed using the right mouse button.</remarks>
|
||||
protected virtual bool HasContextMenu => Element.ContextMenu != null;
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the drag interaction can begin (see <see cref="OnBegin(InputEventArgs)"/>).
|
||||
/// </summary>
|
||||
protected virtual bool CanBegin { get; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the drag interaction can be canceled (see <see cref="OnCancel(InputEventArgs)"/>).
|
||||
/// </summary>
|
||||
protected virtual bool CanCancel { get; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the drag gesture is a toggle, meaning the same gesture can be used to both start and stop the interaction.
|
||||
/// </summary>
|
||||
protected virtual bool IsToggle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the UI element used to calculate the mouse position during the drag interaction.
|
||||
/// </summary>
|
||||
protected IInputElement PositionElement { get; set; }
|
||||
|
||||
private InteractionState _interactionState;
|
||||
private Point _initialPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DragState{TElement}"/> class with a begin gesture.
|
||||
/// </summary>
|
||||
/// <param name="element">The element associated with this state.</param>
|
||||
/// <param name="beginGesture">The gesture used to start the drag interaction.</param>
|
||||
public DragState(TElement element, InputGesture beginGesture) : base(element)
|
||||
{
|
||||
BeginGesture = beginGesture;
|
||||
PositionElement = element;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DragState{TElement}"/> class with begin and cancel gestures.
|
||||
/// </summary>
|
||||
/// <param name="element">The element associated with this state.</param>
|
||||
/// <param name="beginGesture">The gesture used to start the drag interaction.</param>
|
||||
/// <param name="cancelGesture">The gesture used to cancel the drag interaction.</param>
|
||||
public DragState(TElement element, InputGesture beginGesture, InputGesture cancelGesture)
|
||||
: this(element, beginGesture)
|
||||
{
|
||||
CancelGesture = cancelGesture;
|
||||
}
|
||||
|
||||
void IInputHandler.HandleEvent(InputEventArgs e)
|
||||
{
|
||||
if (_interactionState == InteractionState.Ready && TryBeginDragging(e))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_interactionState == InteractionState.Ending && TryEndDragging(e))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_interactionState == InteractionState.InProgress)
|
||||
{
|
||||
if (TryEndDragging(e) || TryCancelDragging(e) || TrySuppressContextMenu(e))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TryHandleEvent(e);
|
||||
}
|
||||
|
||||
#region Interaction logic
|
||||
|
||||
// Begin the interaction on gesture press
|
||||
private bool TryBeginDragging(InputEventArgs e)
|
||||
{
|
||||
if (IsInputEventPressed(e) && CanBegin && BeginGesture.Matches(e.Source, e))
|
||||
{
|
||||
BeginDrag(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryEndDragging(InputEventArgs e)
|
||||
{
|
||||
if (IsInputCaptureLost(e))
|
||||
{
|
||||
EndDrag(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsToggle && _interactionState == InteractionState.InProgress)
|
||||
{
|
||||
return TryDeferToggleInteractionEnd(e);
|
||||
}
|
||||
|
||||
return TryEndInteraction(e);
|
||||
}
|
||||
|
||||
// Delay ending toggle interaction until the gesture is released
|
||||
private bool TryDeferToggleInteractionEnd(InputEventArgs e)
|
||||
{
|
||||
if (IsInputEventPressed(e) && BeginGesture.Matches(e.Source, e))
|
||||
{
|
||||
_interactionState = InteractionState.Ending;
|
||||
HandleEvent(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// End the interaction on gesture release
|
||||
private bool TryEndInteraction(InputEventArgs e)
|
||||
{
|
||||
if (IsInputEventReleased(e) && BeginGesture.Matches(e.Source, e))
|
||||
{
|
||||
EndDrag(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cancel the interaction
|
||||
private bool TryCancelDragging(InputEventArgs e)
|
||||
{
|
||||
if (CanCancel && IsInputEventReleased(e) && CancelGesture?.Matches(e.Source, e) is true)
|
||||
{
|
||||
CancelDrag(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Suppress the context menu if a toggle interaction is in progress
|
||||
private bool TrySuppressContextMenu(InputEventArgs e)
|
||||
{
|
||||
if (IsToggle && e is MouseButtonEventArgs mbe && mbe.ChangedButton == MouseButton.Right)
|
||||
{
|
||||
e.Handled = true;
|
||||
HandleEvent(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void TryHandleEvent(InputEventArgs e)
|
||||
{
|
||||
if (_interactionState == InteractionState.InProgress || _interactionState == InteractionState.Ending)
|
||||
{
|
||||
HandleEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
internal void BeginDrag(InputEventArgs e)
|
||||
{
|
||||
// Avoid stealing mouse capture from other elements
|
||||
if (CanCaptureInput(e))
|
||||
{
|
||||
RequiresInputCapture = IsToggle;
|
||||
|
||||
_interactionState = InteractionState.InProgress;
|
||||
_initialPosition = GetInitialPosition(e);
|
||||
|
||||
HandleEvent(e); // Handle the event, otherwise CaptureMouse will send a MouseMove event and the current event will be handled out of order
|
||||
OnBegin(e);
|
||||
|
||||
e.Handled = true;
|
||||
|
||||
Element.Focus();
|
||||
CaptureInput(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void EndDrag(InputEventArgs e)
|
||||
{
|
||||
_interactionState = InteractionState.Ready;
|
||||
HandleEvent(e);
|
||||
|
||||
// Suppress the context menu if the mouse moved beyond the defined drag threshold
|
||||
if (HasContextMenu && e is MouseButtonEventArgs mbe && mbe.ChangedButton == MouseButton.Right)
|
||||
{
|
||||
double dragThreshold = NodifyEditor.MouseActionSuppressionThreshold * NodifyEditor.MouseActionSuppressionThreshold;
|
||||
double dragDistance = (mbe.GetPosition(PositionElement) - _initialPosition).LengthSquared;
|
||||
|
||||
if (dragDistance > dragThreshold)
|
||||
{
|
||||
OnEnd(e);
|
||||
e.Handled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
OnCancel(e);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
OnEnd(e);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
RequiresInputCapture = false;
|
||||
}
|
||||
|
||||
private void CancelDrag(InputEventArgs e)
|
||||
{
|
||||
_interactionState = InteractionState.Ready;
|
||||
HandleEvent(e);
|
||||
OnCancel(e);
|
||||
|
||||
e.Handled = true;
|
||||
RequiresInputCapture = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the initial position of the input event relative to the <see cref="PositionElement"/>.
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="InputEventArgs"/> representing the input event.</param>
|
||||
/// <remarks>
|
||||
/// This position is used to calculate the drag distance, to determine whether
|
||||
/// the context menu can appear or if the action is considered a drag operation. The behavior is influenced
|
||||
/// by the <see cref="NodifyEditor.MouseActionSuppressionThreshold"/>.
|
||||
/// </remarks>
|
||||
protected virtual Point GetInitialPosition(InputEventArgs e)
|
||||
{
|
||||
if (e is MouseEventArgs me)
|
||||
{
|
||||
return me.GetPosition(PositionElement);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether input capture can be acquired for the <see cref="InputElementState{TElement}.Element" />.
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="InputEventArgs"/> representing the input event.</param>
|
||||
/// <remarks>Must return true if the input is already captured by the current element.</remarks>
|
||||
protected virtual bool CanCaptureInput(InputEventArgs e)
|
||||
=> Mouse.Captured == null || Element.IsMouseCaptured;
|
||||
|
||||
/// <summary>
|
||||
/// Captures input for the element.
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="InputEventArgs"/> representing the input event.</param>
|
||||
protected virtual void CaptureInput(InputEventArgs e)
|
||||
=> Element.CaptureMouse();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether input capture has been lost.
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="InputEventArgs"/> representing the input event.</param>
|
||||
protected virtual bool IsInputCaptureLost(InputEventArgs e)
|
||||
=> e.RoutedEvent == UIElement.LostMouseCaptureEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the given input event represents the release of an input gesture.
|
||||
/// </summary>
|
||||
/// <param name="e">The input event to evaluate.</param>
|
||||
/// <returns>True if the event represents the release of a gesture; otherwise, false.</returns>
|
||||
protected virtual bool IsInputEventReleased(InputEventArgs e)
|
||||
{
|
||||
if (e is MouseButtonEventArgs mbe && mbe.ButtonState == MouseButtonState.Released)
|
||||
return true;
|
||||
|
||||
if (e is KeyEventArgs ke && ke.IsUp)
|
||||
return true;
|
||||
|
||||
if (e is MouseWheelEventArgs mwe && mwe.MiddleButton == MouseButtonState.Released)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the given input event represents the press of an input gesture.
|
||||
/// </summary>
|
||||
/// <param name="e">The input event to evaluate.</param>
|
||||
/// <returns>True if the event represents the press of a gesture; otherwise, false.</returns>
|
||||
protected virtual bool IsInputEventPressed(InputEventArgs e)
|
||||
{
|
||||
if (e is MouseButtonEventArgs mbe && mbe.ButtonState == MouseButtonState.Pressed)
|
||||
return true;
|
||||
|
||||
if (e is KeyEventArgs ke && ke.IsDown)
|
||||
return true;
|
||||
|
||||
if (e is MouseWheelEventArgs mwe && mwe.MiddleButton == MouseButtonState.Pressed)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the drag interaction begins. Override to provide custom behavior.
|
||||
/// </summary>
|
||||
/// <param name="e">The input event that started the interaction.</param>
|
||||
protected virtual void OnBegin(InputEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the drag interaction ends. Override to provide custom behavior.
|
||||
/// </summary>
|
||||
/// <param name="e">The input event that ended the interaction.</param>
|
||||
protected virtual void OnEnd(InputEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the drag interaction is canceled. Override to provide custom behavior.
|
||||
/// </summary>
|
||||
/// <param name="e">The input event that canceled the interaction.</param>
|
||||
protected virtual void OnCancel(InputEventArgs e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Nodify/Interactivity/Gestures/AllGestures.cs
Normal file
12
Nodify/Interactivity/Gestures/AllGestures.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
/// <inheritdoc cref="MultiGesture.Match.All" />
|
||||
public sealed class AllGestures : MultiGesture
|
||||
{
|
||||
public AllGestures(params InputGesture[] gestures) : base(Match.All, gestures)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Nodify/Interactivity/Gestures/AnyGesture.cs
Normal file
12
Nodify/Interactivity/Gestures/AnyGesture.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
/// <inheritdoc cref="MultiGesture.Match.Any" />
|
||||
public sealed class AnyGesture : MultiGesture
|
||||
{
|
||||
public AnyGesture(params InputGesture[] gestures) : base(Match.Any, gestures)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
653
Nodify/Interactivity/Gestures/EditorGestures.cs
Normal file
653
Nodify/Interactivity/Gestures/EditorGestures.cs
Normal file
@@ -0,0 +1,653 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
/// <summary>Gestures used by built-in controls inside the <see cref="NodifyEditor"/>.</summary>
|
||||
public class EditorGestures
|
||||
{
|
||||
public static readonly EditorGestures Mappings = new EditorGestures();
|
||||
|
||||
/// <summary>Gestures for the selection.</summary>
|
||||
public class SelectionGestures
|
||||
{
|
||||
/// <summary>Disable selection gestures.</summary>
|
||||
public static readonly SelectionGestures None = new SelectionGestures(MouseAction.None);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SelectionGestures"/> class with specified mouse action
|
||||
/// and a flag indicating whether modifier keys should be ignored when releasing the mouse button.
|
||||
/// </summary>
|
||||
/// <param name="mouseAction">The mouse action to trigger the gestures.</param>
|
||||
/// <param name="ignoreModifierKeysOnRelease">
|
||||
/// A value indicating whether modifier keys (Alt, Shift, Control) should be ignored when the mouse button is released.
|
||||
/// </param>
|
||||
public SelectionGestures(MouseAction mouseAction, bool ignoreModifierKeysOnRelease)
|
||||
{
|
||||
Replace = new MouseGesture(mouseAction);
|
||||
Remove = new MouseGesture(mouseAction, ModifierKeys.Alt, ignoreModifierKeysOnRelease);
|
||||
Append = new MouseGesture(mouseAction, ModifierKeys.Shift, ignoreModifierKeysOnRelease);
|
||||
Invert = new MouseGesture(mouseAction, ModifierKeys.Control, ignoreModifierKeysOnRelease);
|
||||
Select = new AnyGesture(Replace, Remove, Append, Invert);
|
||||
Cancel = new KeyGesture(Key.Escape);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SelectionGestures"/> class with a specified mouse action.
|
||||
/// Modifier keys will be ignored when releasing the mouse button.
|
||||
/// </summary>
|
||||
/// <param name="mouseAction">The mouse action to trigger the gestures.</param>
|
||||
public SelectionGestures(MouseAction mouseAction)
|
||||
: this(mouseAction, true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SelectionGestures"/> class with a flag indicating
|
||||
/// whether modifier keys should be ignored when releasing the mouse button.
|
||||
/// The default mouse action is <see cref="MouseAction.LeftClick"/>.
|
||||
/// </summary>
|
||||
/// <param name="ignoreModifierKeysOnRelease">
|
||||
/// A value indicating whether modifier keys (Alt, Shift, Control) should be ignored when the mouse button is released.
|
||||
/// </param>
|
||||
public SelectionGestures(bool ignoreModifierKeysOnRelease)
|
||||
: this(MouseAction.LeftClick, ignoreModifierKeysOnRelease)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SelectionGestures"/> class with default values:
|
||||
/// the mouse action is <see cref="MouseAction.LeftClick"/>, and modifier keys are ignored when releasing the mouse button.
|
||||
/// </summary>
|
||||
public SelectionGestures() : this(true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Gesture to replace previous selection with the selected items.</summary>
|
||||
/// <remarks>Defaults to <see cref="MouseAction.LeftClick"/>.</remarks>
|
||||
public InputGestureRef Replace { get; }
|
||||
|
||||
/// <summary>Gesture to remove the selected items from the previous selection.</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.Alt"/>+<see cref="MouseAction.LeftClick"/>.</remarks>
|
||||
public InputGestureRef Remove { get; }
|
||||
|
||||
/// <summary>Gesture to add the new selected items to the previous selection.</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.Shift"/>+<see cref="MouseAction.LeftClick"/>.</remarks>
|
||||
public InputGestureRef Append { get; }
|
||||
|
||||
/// <summary>Gesture to invert the selected items.</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.Control"/>+<see cref="MouseAction.LeftClick"/>.</remarks>
|
||||
public InputGestureRef Invert { get; }
|
||||
|
||||
/// <summary>Cancel the current selection operation reverting to the previous selection.</summary>
|
||||
/// <remarks>Defaults to <see cref="Key.Escape"/>.</remarks>
|
||||
public InputGestureRef Cancel { get; }
|
||||
|
||||
/// <summary>Gesture used to start selecting using a <see cref="SelectionGestures"/> strategy.</summary>
|
||||
public InputGestureRef Select { get; }
|
||||
|
||||
/// <summary>Copies from the specified gestures.</summary>
|
||||
/// <param name="gestures">The gestures to copy.</param>
|
||||
public void Apply(SelectionGestures gestures)
|
||||
{
|
||||
Replace.Value = gestures.Replace.Value;
|
||||
Remove.Value = gestures.Remove.Value;
|
||||
Append.Value = gestures.Append.Value;
|
||||
Invert.Value = gestures.Invert.Value;
|
||||
Select.Value = gestures.Select.Value;
|
||||
Cancel.Value = gestures.Cancel.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unbinds the all the gestures used for selection.
|
||||
/// </summary>
|
||||
public void Unbind()
|
||||
=> Apply(None);
|
||||
}
|
||||
|
||||
/// <summary>Gestures for the item containers.</summary>
|
||||
public class ItemContainerGestures
|
||||
{
|
||||
public ItemContainerGestures()
|
||||
{
|
||||
Selection = new SelectionGestures();
|
||||
Selection.Select.Value = new AnyGesture(
|
||||
Selection.Replace,
|
||||
Selection.Remove,
|
||||
Selection.Append,
|
||||
Selection.Invert);
|
||||
|
||||
Drag = new AnyGesture(Selection.Replace, Selection.Remove, Selection.Append, Selection.Invert);
|
||||
CancelAction = new AnyGesture(new MouseGesture(MouseAction.RightClick), new KeyGesture(Key.Escape));
|
||||
}
|
||||
|
||||
/// <summary>Gesture to select the container using a <see cref="SelectionGestures"/> strategy.</summary>
|
||||
/// <remarks>Defaults to <see cref="MouseAction.LeftClick"/> or any of the <see cref="SelectionGestures"/> gestures.</remarks>
|
||||
public SelectionGestures Selection { get; }
|
||||
|
||||
/// <summary>Gesture to start and complete a dragging operation.</summary>
|
||||
/// <remarks>Using a <see cref="Selection"/> strategy to drag from a new selection.
|
||||
/// <br /> Defaults to any of the <see cref="Selection"/> gestures.
|
||||
/// </remarks>
|
||||
public InputGestureRef Drag { get; }
|
||||
|
||||
/// <summary>Gesture to cancel the dragging operation.</summary>
|
||||
/// <remarks>Defaults to <see cref="MouseAction.RightClick"/> or <see cref="Key.Escape"/>.</remarks>
|
||||
public InputGestureRef CancelAction { get; }
|
||||
|
||||
/// <summary>Copies from the specified gestures.</summary>
|
||||
/// <param name="gestures">The gestures to copy.</param>
|
||||
public void Apply(ItemContainerGestures gestures)
|
||||
{
|
||||
Selection.Apply(gestures.Selection);
|
||||
Drag.Value = gestures.Drag.Value;
|
||||
CancelAction.Value = gestures.CancelAction.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unbinds all the gestures.
|
||||
/// </summary>
|
||||
public void Unbind()
|
||||
{
|
||||
Selection.Unbind();
|
||||
Drag.Unbind();
|
||||
CancelAction.Unbind();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keyboard gestures used for navigating the editor and moving selected items.
|
||||
/// </summary>
|
||||
public class DirectionalNavigationGestures
|
||||
{
|
||||
public DirectionalNavigationGestures(ModifierKeys modifierKeys = ModifierKeys.None)
|
||||
{
|
||||
Up = new KeyGesture(Key.Up, modifierKeys);
|
||||
Left = new KeyGesture(Key.Left, modifierKeys);
|
||||
Down = new KeyGesture(Key.Down, modifierKeys);
|
||||
Right = new KeyGesture(Key.Right, modifierKeys);
|
||||
}
|
||||
|
||||
public DirectionalNavigationGestures(Key triggerKey, ModifierKeys modifierKeys = ModifierKeys.None, bool repeated = false)
|
||||
{
|
||||
Up = new KeyComboGesture(triggerKey, Key.Up, modifierKeys) { AllowRepeatingComboKey = repeated };
|
||||
Left = new KeyComboGesture(triggerKey, Key.Left, modifierKeys) { AllowRepeatingComboKey = repeated };
|
||||
Down = new KeyComboGesture(triggerKey, Key.Down, modifierKeys) { AllowRepeatingComboKey = repeated };
|
||||
Right = new KeyComboGesture(triggerKey, Key.Right, modifierKeys) { AllowRepeatingComboKey = repeated };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gesture used for navigating or moving upward.
|
||||
/// </summary>
|
||||
public InputGestureRef Up { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gesture used for navigating or moving left.
|
||||
/// </summary>
|
||||
public InputGestureRef Left { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gesture used for navigating or moving downward.
|
||||
/// </summary>
|
||||
public InputGestureRef Down { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gesture used for navigating or moving right.
|
||||
/// </summary>
|
||||
public InputGestureRef Right { get; }
|
||||
|
||||
/// <summary>Copies from the specified gestures.</summary>
|
||||
/// <param name="gestures">The gestures to copy.</param>
|
||||
public void Apply(DirectionalNavigationGestures gestures)
|
||||
{
|
||||
Up.Value = gestures.Up.Value;
|
||||
Left.Value = gestures.Left.Value;
|
||||
Down.Value = gestures.Down.Value;
|
||||
Right.Value = gestures.Right.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unbinds all the gestures.
|
||||
/// </summary>
|
||||
public void Unbind()
|
||||
{
|
||||
Up.Unbind();
|
||||
Left.Unbind();
|
||||
Down.Unbind();
|
||||
Right.Unbind();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gestures for the editor.</summary>
|
||||
public class NodifyEditorGestures
|
||||
{
|
||||
/// <summary>
|
||||
/// Keyboard gestures used for navigation, selection, and manipulation in the editor.
|
||||
/// </summary>
|
||||
public class KeyboardGestures
|
||||
{
|
||||
public KeyboardGestures()
|
||||
{
|
||||
Pan = new DirectionalNavigationGestures(Key.Space, repeated: true);
|
||||
DragSelection = new DirectionalNavigationGestures(ModifierKeys.Control);
|
||||
NavigateSelection = new DirectionalNavigationGestures(ModifierKeys.None);
|
||||
ToggleSelected = new AnyGesture(new KeyGesture(Key.Space), new KeyGesture(Key.Enter));
|
||||
DeselectAll = new KeyGesture(Key.Escape);
|
||||
NextNavigationLayer = new KeyGesture(Key.OemCloseBrackets, ModifierKeys.Control);
|
||||
PrevNavigationLayer = new KeyGesture(Key.OemOpenBrackets, ModifierKeys.Control);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Directional gestures used for panning the viewport.
|
||||
/// </summary>
|
||||
/// <remarks>Defaults to <see cref="Key.Space"/>+arrow keys.</remarks>
|
||||
public DirectionalNavigationGestures Pan { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Directional gestures used for dragging the selected items.
|
||||
/// </summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.Control"/>+arrow keys.</remarks>
|
||||
public DirectionalNavigationGestures DragSelection { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Directional gestures used to navigate the selection focus (e.g., between nodes).
|
||||
/// </summary>
|
||||
/// <remarks>Defaults to arrow keys.</remarks>
|
||||
public DirectionalNavigationGestures NavigateSelection { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gesture used to toggle the selected state of the currently focused item.
|
||||
/// </summary>
|
||||
/// <remarks>Defaults to <see cref="Key.Space"/></remarks>
|
||||
public InputGestureRef ToggleSelected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gesture used to clear the current selection.
|
||||
/// </summary>
|
||||
/// <remarks>Defaults to <see cref="Key.Escape"/>.</remarks>
|
||||
public InputGestureRef DeselectAll { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gesture used to activate the previous keyboard navigation layer.
|
||||
/// </summary>
|
||||
/// <remarks><see cref="ModifierKeys.Control"/>+<see cref="Key.OemCloseBrackets"/>.</remarks>
|
||||
public InputGestureRef NextNavigationLayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gesture used to activate the next keyboard navigation layer.
|
||||
/// </summary>
|
||||
/// <remarks><see cref="ModifierKeys.Control"/>+<see cref="Key.OemOpenBrackets"/>.</remarks>
|
||||
public InputGestureRef PrevNavigationLayer { get; }
|
||||
|
||||
/// <summary>Copies from the specified gestures.</summary>
|
||||
/// <param name="gestures">The gestures to copy.</param>
|
||||
public void Apply(KeyboardGestures gestures)
|
||||
{
|
||||
Pan.Apply(gestures.Pan);
|
||||
DragSelection.Apply(gestures.DragSelection);
|
||||
NavigateSelection.Apply(gestures.NavigateSelection);
|
||||
ToggleSelected.Value = gestures.ToggleSelected.Value;
|
||||
DeselectAll.Value = gestures.DeselectAll.Value;
|
||||
NextNavigationLayer.Value = gestures.NextNavigationLayer.Value;
|
||||
PrevNavigationLayer.Value = gestures.PrevNavigationLayer.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unbinds all the gestures.
|
||||
/// </summary>
|
||||
public void Unbind()
|
||||
{
|
||||
Pan.Unbind();
|
||||
DragSelection.Unbind();
|
||||
NavigateSelection.Unbind();
|
||||
ToggleSelected.Unbind();
|
||||
DeselectAll.Unbind();
|
||||
NextNavigationLayer.Unbind();
|
||||
PrevNavigationLayer.Unbind();
|
||||
}
|
||||
}
|
||||
|
||||
public NodifyEditorGestures()
|
||||
{
|
||||
Keyboard = new KeyboardGestures();
|
||||
Selection = new SelectionGestures();
|
||||
SelectAll = ApplicationCommands.SelectAll.InputGestures[0].AsRef();
|
||||
Cutting = new MouseGesture(MouseAction.LeftClick, ModifierKeys.Alt | ModifierKeys.Shift, true);
|
||||
PushItems = new MouseGesture(MouseAction.LeftClick, ModifierKeys.Control | ModifierKeys.Shift, true);
|
||||
Pan = new AnyGesture(new MouseGesture(MouseAction.RightClick), new MouseGesture(MouseAction.MiddleClick));
|
||||
ZoomModifierKey = ModifierKeys.None;
|
||||
ZoomIn = new AnyGesture(new KeyGesture(Key.OemPlus, ModifierKeys.Control), new KeyGesture(Key.Add, ModifierKeys.Control));
|
||||
ZoomOut = new AnyGesture(new KeyGesture(Key.OemMinus, ModifierKeys.Control), new KeyGesture(Key.Subtract, ModifierKeys.Control));
|
||||
ResetViewport = new KeyGesture(Key.Home);
|
||||
FitToScreen = new KeyGesture(Key.Home, ModifierKeys.Shift);
|
||||
CancelAction = new AnyGesture(new MouseGesture(MouseAction.RightClick), new KeyGesture(Key.Escape));
|
||||
PanWithMouseWheel = false;
|
||||
PanHorizontalModifierKey = ModifierKeys.Shift;
|
||||
PanVerticalModifierKey = ModifierKeys.None;
|
||||
}
|
||||
|
||||
public KeyboardGestures Keyboard { get; }
|
||||
|
||||
/// <summary>Gesture used to start selecting using a <see cref="SelectionGestures"/> strategy.</summary>
|
||||
public SelectionGestures Selection { get; }
|
||||
|
||||
/// <summary>Gesture used to select all <see cref="Nodify.ItemContainer"/>s in the editor.</summary>
|
||||
public InputGestureRef SelectAll { get; }
|
||||
|
||||
/// <summary>Gesture used to start cutting connections.</summary>
|
||||
public InputGestureRef Cutting { get; }
|
||||
|
||||
/// <summary>Gesture used to start panning.</summary>
|
||||
/// <remarks>Defaults to <see cref="MouseAction.RightClick"/> or <see cref="MouseAction.MiddleClick"/>.</remarks>
|
||||
public InputGestureRef Pan { get; }
|
||||
|
||||
/// <summary>Whether panning using mouse wheel is allowed.</summary>
|
||||
/// <remarks>Set the <see cref="ZoomModifierKey"/> to allow zooming using the mouse wheel.</remarks>
|
||||
public bool PanWithMouseWheel { get; set; }
|
||||
|
||||
/// <summary>The modifier key required to start panning vertically with the mouse wheel (see <see cref="PanWithMouseWheel"/>)</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.None"/>.</remarks>
|
||||
public ModifierKeys PanVerticalModifierKey { get; set; }
|
||||
|
||||
/// <summary>The modifier key required to start panning horizontally with the mouse wheel (see <see cref="PanWithMouseWheel"/>)</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.Shift"/>.</remarks>
|
||||
public ModifierKeys PanHorizontalModifierKey { get; set; }
|
||||
|
||||
/// <summary>Gesture used to start pushing.</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.Control"/>+<see cref="ModifierKeys.Shift"/>+<see cref="MouseAction.LeftClick"/>.</remarks>
|
||||
public InputGestureRef PushItems { get; }
|
||||
|
||||
/// <summary>The key modifier required to start zooming by mouse wheel.</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.None"/>.</remarks>
|
||||
public ModifierKeys ZoomModifierKey { get; set; }
|
||||
|
||||
/// <summary>Gesture used to zoom in.</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.Control"/>+<see cref="Key.OemPlus"/>.</remarks>
|
||||
public InputGestureRef ZoomIn { get; }
|
||||
|
||||
/// <summary>Gesture used to zoom out.</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.Control"/>+<see cref="Key.OemMinus"/>.</remarks>
|
||||
public InputGestureRef ZoomOut { get; }
|
||||
|
||||
/// <summary>Gesture used to move the editor's viewport location to (0, 0) and set the zoom to 1.</summary>
|
||||
/// <remarks>Defaults to <see cref="Key.Home"/>.</remarks>
|
||||
public InputGestureRef ResetViewport { get; }
|
||||
|
||||
/// <summary>Gesture used to fit as many containers as possible into the viewport.</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.Shift"/>+<see cref="Key.Home"/>.</remarks>
|
||||
public InputGestureRef FitToScreen { get; }
|
||||
|
||||
/// <summary>Gesture to cancel the current operation.</summary>
|
||||
/// <remarks>Defaults to <see cref="MouseAction.RightClick"/> or <see cref="Key.Escape"/>.</remarks>
|
||||
public InputGestureRef CancelAction { get; }
|
||||
|
||||
/// <summary>Copies from the specified gestures.</summary>
|
||||
/// <param name="gestures">The gestures to copy.</param>
|
||||
public void Apply(NodifyEditorGestures gestures)
|
||||
{
|
||||
Keyboard.Apply(gestures.Keyboard);
|
||||
Selection.Apply(gestures.Selection);
|
||||
SelectAll.Value = gestures.SelectAll.Value;
|
||||
Cutting.Value = gestures.Cutting.Value;
|
||||
PushItems.Value = gestures.PushItems.Value;
|
||||
Pan.Value = gestures.Pan.Value;
|
||||
ZoomModifierKey = gestures.ZoomModifierKey;
|
||||
ZoomIn.Value = gestures.ZoomIn.Value;
|
||||
ZoomOut.Value = gestures.ZoomOut.Value;
|
||||
ResetViewport.Value = gestures.ResetViewport.Value;
|
||||
FitToScreen.Value = gestures.FitToScreen.Value;
|
||||
CancelAction.Value = gestures.CancelAction.Value;
|
||||
PanWithMouseWheel = gestures.PanWithMouseWheel;
|
||||
PanHorizontalModifierKey = gestures.PanHorizontalModifierKey;
|
||||
PanVerticalModifierKey = gestures.PanVerticalModifierKey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unbinds all the gestures.
|
||||
/// </summary>
|
||||
public void Unbind()
|
||||
{
|
||||
Keyboard.Unbind();
|
||||
Selection.Unbind();
|
||||
SelectAll.Unbind();
|
||||
Cutting.Unbind();
|
||||
Pan.Unbind();
|
||||
PushItems.Unbind();
|
||||
ZoomIn.Unbind();
|
||||
ZoomOut.Unbind();
|
||||
ResetViewport.Unbind();
|
||||
FitToScreen.Unbind();
|
||||
CancelAction.Unbind();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gestures used by the <see cref="Connector"/>.</summary>
|
||||
public class ConnectorGestures
|
||||
{
|
||||
public ConnectorGestures()
|
||||
{
|
||||
Disconnect = new AnyGesture(new MouseGesture(MouseAction.LeftClick, ModifierKeys.Alt), new KeyGesture(Key.Delete));
|
||||
Connect = new AnyGesture(new MouseGesture(MouseAction.LeftClick), new KeyGesture(Key.Space));
|
||||
CancelAction = new AnyGesture(new MouseGesture(MouseAction.RightClick), new KeyGesture(Key.Escape));
|
||||
}
|
||||
|
||||
/// <summary>Gesture to call the <see cref="Connector.DisconnectCommand"/>.</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.Alt"/>+<see cref="MouseAction.LeftClick"/> or <see cref="Key.Delete"/>.</remarks>
|
||||
public InputGestureRef Disconnect { get; }
|
||||
|
||||
/// <summary>Gesture to start and complete a pending connection.</summary>
|
||||
/// <remarks>Defaults to <see cref="MouseAction.LeftClick"/>.</remarks>
|
||||
public InputGestureRef Connect { get; }
|
||||
|
||||
/// <summary>Gesture to cancel the pending connection.</summary>
|
||||
/// <remarks>Defaults to <see cref="MouseAction.RightClick"/> or <see cref="Key.Escape"/>.</remarks>
|
||||
public InputGestureRef CancelAction { get; }
|
||||
|
||||
/// <summary>Copies from the specified gestures.</summary>
|
||||
/// <param name="gestures">The gestures to copy.</param>
|
||||
public void Apply(ConnectorGestures gestures)
|
||||
{
|
||||
Disconnect.Value = gestures.Disconnect.Value;
|
||||
Connect.Value = gestures.Connect.Value;
|
||||
CancelAction.Value = gestures.CancelAction.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unbinds all the gestures.
|
||||
/// </summary>
|
||||
public void Unbind()
|
||||
{
|
||||
Disconnect.Unbind();
|
||||
Connect.Unbind();
|
||||
CancelAction.Unbind();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gestures used by the <see cref="BaseConnection"/>.</summary>
|
||||
public class ConnectionGestures
|
||||
{
|
||||
public ConnectionGestures()
|
||||
{
|
||||
Split = new MouseGesture(MouseAction.LeftDoubleClick);
|
||||
Selection = new SelectionGestures(MouseAction.LeftClick);
|
||||
Disconnect = new MouseGesture(MouseAction.LeftClick, ModifierKeys.Alt);
|
||||
}
|
||||
|
||||
/// <summary>Gesture to call the <see cref="BaseConnection.SplitCommand"/> command.</summary>
|
||||
/// <remarks>Defaults to <see cref="MouseAction.LeftDoubleClick"/>.</remarks>
|
||||
public InputGestureRef Split { get; }
|
||||
|
||||
/// <summary>Gesture used to start selecting using a <see cref="SelectionGestures"/> strategy.</summary>
|
||||
public SelectionGestures Selection { get; }
|
||||
|
||||
/// <summary>Gesture to call the <see cref="BaseConnection.DisconnectCommand"/> command.</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.Alt"/>+<see cref="MouseAction.LeftClick"/>.</remarks>
|
||||
public InputGestureRef Disconnect { get; }
|
||||
|
||||
/// <summary>Copies from the specified gestures.</summary>
|
||||
/// <param name="gestures">The gestures to copy.</param>
|
||||
public void Apply(ConnectionGestures gestures)
|
||||
{
|
||||
Split.Value = gestures.Split.Value;
|
||||
Disconnect.Value = gestures.Disconnect.Value;
|
||||
Selection.Apply(gestures.Selection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unbinds all the gestures.
|
||||
/// </summary>
|
||||
public void Unbind()
|
||||
{
|
||||
Split.Unbind();
|
||||
Selection.Unbind();
|
||||
Disconnect.Unbind();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gestures for the <see cref="GroupingNode"/>.</summary>
|
||||
public class GroupingNodeGestures
|
||||
{
|
||||
public GroupingNodeGestures()
|
||||
{
|
||||
SwitchMovementMode = ModifierKeys.Shift;
|
||||
ToggleContentSelection = new AnyGesture(new KeyGesture(Key.Space, ModifierKeys.Control), new KeyGesture(Key.Enter, ModifierKeys.Control));
|
||||
}
|
||||
|
||||
/// <summary>The key modifier that will toggle between <see cref="GroupingMovementMode"/>s.</summary>
|
||||
/// <remarks>The modifier must be allowed by the <see cref="ItemContainer.Drag"/> gesture.
|
||||
/// <br /> Defaults to <see cref="ModifierKeys.Shift"/>.
|
||||
/// </remarks>
|
||||
public ModifierKeys SwitchMovementMode { get; set; }
|
||||
|
||||
/// <summary>Gesture to toggle the content selection of the <see cref="GroupingNode"/> when it is selected.</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.Control"/>+<see cref="Key.Space"/>.</remarks>
|
||||
public InputGestureRef ToggleContentSelection { get; }
|
||||
|
||||
/// <summary>Copies from the specified gestures.</summary>
|
||||
/// <param name="gestures">The gestures to copy.</param>
|
||||
public void Apply(GroupingNodeGestures gestures)
|
||||
{
|
||||
SwitchMovementMode = gestures.SwitchMovementMode;
|
||||
ToggleContentSelection.Value = gestures.ToggleContentSelection.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unbinds all the gestures.
|
||||
/// </summary>
|
||||
public void Unbind()
|
||||
{
|
||||
ToggleContentSelection.Unbind();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gestures used by the <see cref="Nodify.Minimap"/> control.</summary>
|
||||
public class MinimapGestures
|
||||
{
|
||||
public MinimapGestures()
|
||||
{
|
||||
Pan = new DirectionalNavigationGestures();
|
||||
DragViewport = new MouseGesture(MouseAction.LeftClick);
|
||||
ResetViewport = new KeyGesture(Key.Home);
|
||||
CancelAction = new AnyGesture(new MouseGesture(MouseAction.RightClick), new KeyGesture(Key.Escape));
|
||||
ZoomIn = new AnyGesture(new KeyGesture(Key.OemPlus, ModifierKeys.Control), new KeyGesture(Key.Add, ModifierKeys.Control));
|
||||
ZoomOut = new AnyGesture(new KeyGesture(Key.OemMinus, ModifierKeys.Control), new KeyGesture(Key.Subtract, ModifierKeys.Control));
|
||||
ZoomModifierKey = ModifierKeys.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Directional gestures used for panning the viewport.
|
||||
/// </summary>
|
||||
/// <remarks>Defaults to <see cref="Key.Space"/>+arrow keys.</remarks>
|
||||
public DirectionalNavigationGestures Pan { get; }
|
||||
|
||||
/// <summary>Gesture to move the viewport inside the <see cref="Minimap" />.</summary>
|
||||
public InputGestureRef DragViewport { get; }
|
||||
|
||||
/// <summary>Gesture to move the viewport inside the <see cref="Minimap" />.</summary>
|
||||
public InputGestureRef ResetViewport { get; }
|
||||
|
||||
/// <summary>Gesture to cancel the panning operation.</summary>
|
||||
/// <remarks>Defaults to <see cref="MouseAction.RightClick"/> or <see cref="Key.Escape"/>.</remarks>
|
||||
public InputGestureRef CancelAction { get; }
|
||||
|
||||
/// <summary>Gesture used to zoom in.</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.Control"/>+<see cref="Key.OemPlus"/>.</remarks>
|
||||
public InputGestureRef ZoomIn { get; }
|
||||
|
||||
/// <summary>Gesture used to zoom out.</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.Control"/>+<see cref="Key.OemMinus"/>.</remarks>
|
||||
public InputGestureRef ZoomOut { get; }
|
||||
|
||||
/// <summary>The key modifier required to start zooming by mouse wheel.</summary>
|
||||
/// <remarks>Defaults to <see cref="ModifierKeys.None"/>.</remarks>
|
||||
public ModifierKeys ZoomModifierKey { get; set; }
|
||||
|
||||
/// <summary>Copies from the specified gestures.</summary>
|
||||
/// <param name="gestures">The gestures to copy.</param>
|
||||
public void Apply(MinimapGestures gestures)
|
||||
{
|
||||
Pan.Apply(gestures.Pan);
|
||||
ZoomIn.Value = gestures.ZoomIn.Value;
|
||||
ZoomOut.Value = gestures.ZoomOut.Value;
|
||||
ResetViewport.Value = gestures.ResetViewport.Value;
|
||||
DragViewport.Value = gestures.DragViewport.Value;
|
||||
CancelAction.Value = gestures.CancelAction.Value;
|
||||
ZoomModifierKey = gestures.ZoomModifierKey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unbinds all the gestures.
|
||||
/// </summary>
|
||||
public void Unbind()
|
||||
{
|
||||
Pan.Unbind();
|
||||
ZoomIn.Unbind();
|
||||
ZoomOut.Unbind();
|
||||
ResetViewport.Unbind();
|
||||
DragViewport.Unbind();
|
||||
CancelAction.Unbind();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gestures for the editor.</summary>
|
||||
public NodifyEditorGestures Editor { get; } = new NodifyEditorGestures();
|
||||
|
||||
/// <summary>Gestures for the item container.</summary>
|
||||
public ItemContainerGestures ItemContainer { get; } = new ItemContainerGestures();
|
||||
|
||||
/// <summary>Gestures for the connector.</summary>
|
||||
public ConnectorGestures Connector { get; } = new ConnectorGestures();
|
||||
|
||||
/// <summary>Gestures for the connection.</summary>
|
||||
public ConnectionGestures Connection { get; } = new ConnectionGestures();
|
||||
|
||||
/// <summary>Gestures for the grouping node.</summary>
|
||||
public GroupingNodeGestures GroupingNode { get; } = new GroupingNodeGestures();
|
||||
|
||||
/// <summary>Gestures for the minimap.</summary>
|
||||
public MinimapGestures Minimap { get; } = new MinimapGestures();
|
||||
|
||||
/// <summary>Copies from the specified gestures.</summary>
|
||||
/// <param name="gestures">The gestures to copy.</param>
|
||||
public void Apply(EditorGestures gestures)
|
||||
{
|
||||
Editor.Apply(gestures.Editor);
|
||||
ItemContainer.Apply(gestures.ItemContainer);
|
||||
Connector.Apply(gestures.Connector);
|
||||
Connection.Apply(gestures.Connection);
|
||||
GroupingNode.Apply(gestures.GroupingNode);
|
||||
Minimap.Apply(gestures.Minimap);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unbinds all the gestures used by the editor and its controls.
|
||||
/// </summary>
|
||||
public void Unbind()
|
||||
{
|
||||
Editor.Unbind();
|
||||
ItemContainer.Unbind();
|
||||
Connector.Unbind();
|
||||
Connection.Unbind();
|
||||
Minimap.Unbind();
|
||||
}
|
||||
}
|
||||
}
|
||||
58
Nodify/Interactivity/Gestures/InputGestureRef.cs
Normal file
58
Nodify/Interactivity/Gestures/InputGestureRef.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
/// <summary>
|
||||
/// An input gesture that allows changing its logic at runtime without changing its reference.
|
||||
/// Useful for classes that capture the object reference without the posibility of updating it. (e.g. <see cref="EditorCommands"/>)
|
||||
/// </summary>
|
||||
public sealed class InputGestureRef : InputGesture
|
||||
{
|
||||
/// <summary>The referenced gesture.</summary>
|
||||
public InputGesture Value { get; set; } = MultiGesture.None;
|
||||
|
||||
private InputGestureRef() { }
|
||||
|
||||
internal InputGestureRef(InputGesture gesture)
|
||||
{
|
||||
Value = gesture;
|
||||
}
|
||||
|
||||
public override bool Matches(object targetElement, InputEventArgs inputEventArgs)
|
||||
{
|
||||
return Value.Matches(targetElement, inputEventArgs);
|
||||
}
|
||||
|
||||
public static implicit operator InputGestureRef(MouseGesture gesture)
|
||||
=> new InputGestureRef { Value = gesture };
|
||||
|
||||
public static implicit operator InputGestureRef(System.Windows.Input.MouseGesture gesture)
|
||||
=> new InputGestureRef { Value = gesture };
|
||||
|
||||
public static implicit operator InputGestureRef(KeyGesture gesture)
|
||||
=> new InputGestureRef { Value = gesture };
|
||||
|
||||
public static implicit operator InputGestureRef(MultiGesture gesture)
|
||||
=> new InputGestureRef { Value = gesture };
|
||||
|
||||
/// <summary>
|
||||
/// Unbinds the current gesture.
|
||||
/// </summary>
|
||||
public void Unbind()
|
||||
=> Value = MultiGesture.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="InputGestureRef"/> class.
|
||||
/// </summary>
|
||||
public static class InputGestureRefExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="InputGestureRef"/> from the specified gesture.
|
||||
/// </summary>
|
||||
public static InputGestureRef AsRef(this InputGesture gesture)
|
||||
{
|
||||
return new InputGestureRef(gesture);
|
||||
}
|
||||
}
|
||||
}
|
||||
131
Nodify/Interactivity/Gestures/KeyComboGesture.cs
Normal file
131
Nodify/Interactivity/Gestures/KeyComboGesture.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a keyboard gesture that requires a trigger key to be held down
|
||||
/// before pressing a combo key. For example, press and hold Space, then press Left arrow.
|
||||
/// </summary>
|
||||
public class KeyComboGesture : KeyGesture
|
||||
{
|
||||
private static readonly WeakReferenceCollection<KeyComboGesture> _allCombos = new WeakReferenceCollection<KeyComboGesture>(16);
|
||||
|
||||
private bool _isTriggerDown;
|
||||
private int _comboCounter;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the combo gesture has been performed at least once.
|
||||
/// </summary>
|
||||
private bool HasBeenPerformedAtLeastOnce => _comboCounter > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the key that must be pressed first to activate this combo gesture.
|
||||
/// </summary>
|
||||
public Key TriggerKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the combo key can be repeatedly triggered
|
||||
/// without releasing the trigger key.
|
||||
/// </summary>
|
||||
public bool AllowRepeatingComboKey { get; set; }
|
||||
|
||||
static KeyComboGesture()
|
||||
{
|
||||
EventManager.RegisterClassHandler(typeof(UIElement), UIElement.PreviewKeyUpEvent, new KeyEventHandler(HandleKeyUp), true);
|
||||
EventManager.RegisterClassHandler(typeof(UIElement), UIElement.LostKeyboardFocusEvent, new KeyboardFocusChangedEventHandler(HandleFocusLost), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="KeyComboGesture"/> class with the specified trigger and combo keys.
|
||||
/// </summary>
|
||||
/// <param name="triggerKey">The key that must be pressed first.</param>
|
||||
/// <param name="comboKey">The combo key pressed while the trigger key is held.</param>
|
||||
public KeyComboGesture(Key triggerKey, Key comboKey) : this(triggerKey, comboKey, ModifierKeys.None, string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="KeyComboGesture"/> class with the specified trigger and combo keys and modifiers.
|
||||
/// </summary>
|
||||
/// <param name="triggerKey">The key that must be pressed first.</param>
|
||||
/// <param name="comboKey">The combo key pressed while the trigger key is held.</param>
|
||||
/// <param name="modifiers">Any modifier keys required for the combo key.</param>
|
||||
public KeyComboGesture(Key triggerKey, Key comboKey, ModifierKeys modifiers) : this(triggerKey, comboKey, modifiers, string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="KeyComboGesture"/> class with the specified trigger key,
|
||||
/// combo key, modifiers, and display string.
|
||||
/// </summary>
|
||||
/// <param name="triggerKey">The key that must be pressed first.</param>
|
||||
/// <param name="comboKey">The combo key pressed while the trigger key is held.</param>
|
||||
/// <param name="modifiers">Any modifier keys required for the combo key.</param>
|
||||
/// <param name="displayString">The display string representing the gesture.</param>
|
||||
public KeyComboGesture(Key triggerKey, Key comboKey, ModifierKeys modifiers, string displayString) : base(comboKey, modifiers, displayString)
|
||||
{
|
||||
TriggerKey = triggerKey;
|
||||
_allCombos.Add(this);
|
||||
}
|
||||
|
||||
private static void HandleFocusLost(object sender, KeyboardFocusChangedEventArgs e)
|
||||
{
|
||||
foreach (var combo in _allCombos)
|
||||
{
|
||||
combo.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleKeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
foreach (var combo in _allCombos)
|
||||
{
|
||||
if (e.Key == combo.TriggerKey)
|
||||
{
|
||||
// We don't want to handle the event if only the trigger key was pressed.
|
||||
if (combo.HasBeenPerformedAtLeastOnce)
|
||||
{
|
||||
e.Handled = true;
|
||||
}
|
||||
combo.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_isTriggerDown = false;
|
||||
_comboCounter = 0;
|
||||
}
|
||||
|
||||
public override bool Matches(object targetElement, InputEventArgs inputEventArgs)
|
||||
{
|
||||
if (inputEventArgs is KeyEventArgs { IsDown: true } keyArgs)
|
||||
{
|
||||
if (keyArgs.Key == TriggerKey)
|
||||
{
|
||||
_isTriggerDown = true;
|
||||
}
|
||||
|
||||
// The combo key only triggers the combo on key down
|
||||
bool matches = _isTriggerDown && base.Matches(targetElement, inputEventArgs);
|
||||
if (!matches)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_comboCounter++;
|
||||
|
||||
if (!AllowRepeatingComboKey)
|
||||
{
|
||||
_isTriggerDown = false;
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
153
Nodify/Interactivity/Gestures/MouseGesture.cs
Normal file
153
Nodify/Interactivity/Gestures/MouseGesture.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using System.Linq;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a mouse gesture that optionally includes a specific key press as part of the gesture.
|
||||
/// </summary>
|
||||
public sealed class MouseGesture : System.Windows.Input.MouseGesture
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the key that must be pressed to match this gesture.
|
||||
/// </summary>
|
||||
public Key Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to ignore modifier keys when releasing the mouse button.
|
||||
/// </summary>
|
||||
public bool IgnoreModifierKeysOnRelease { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MouseGesture"/> class with the specified mouse action, modifier keys, and a specific key.
|
||||
/// </summary>
|
||||
/// <param name="action">The action associated with this gesture.</param>
|
||||
/// <param name="modifiers">The modifiers associated with this gesture.</param>
|
||||
/// <param name="key">The key required to match the gesture.</param>
|
||||
public MouseGesture(MouseAction action, ModifierKeys modifiers, Key key) : base(action, modifiers)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MouseGesture"/> class with the specified mouse action and key.
|
||||
/// </summary>
|
||||
/// <param name="action">The action associated with this gesture.</param>
|
||||
/// <param name="key">The key required to match the gesture.</param>
|
||||
public MouseGesture(MouseAction action, Key key) : base(action)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MouseGesture(MouseAction action, ModifierKeys modifiers)
|
||||
: base(action, modifiers)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MouseGesture"/> class with the specified mouse action and modifier keys.
|
||||
/// </summary>
|
||||
/// <param name="action">The action associated with this gesture.</param>
|
||||
/// <param name="modifiers">The modifiers required to match the gesture.</param>
|
||||
/// <param name="ignoreModifierKeysOnRelease">Whether to ignore modifiers when releasing the mouse button.</param>
|
||||
public MouseGesture(MouseAction action, ModifierKeys modifiers, bool ignoreModifierKeysOnRelease)
|
||||
: base(action, modifiers)
|
||||
{
|
||||
IgnoreModifierKeysOnRelease = ignoreModifierKeysOnRelease;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MouseGesture(MouseAction action) : base(action)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MouseGesture()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Matches(object targetElement, InputEventArgs inputEventArgs)
|
||||
{
|
||||
if (inputEventArgs is MouseButtonEventArgs || inputEventArgs is MouseWheelEventArgs)
|
||||
{
|
||||
bool matches = base.Matches(targetElement, inputEventArgs);
|
||||
|
||||
if (IgnoreModifierKeysOnRelease && IsButtonReleased(inputEventArgs))
|
||||
{
|
||||
ModifierKeys prevModifiers = Modifiers;
|
||||
Modifiers = ModifierKeys.None;
|
||||
matches |= base.Matches(targetElement, inputEventArgs);
|
||||
Modifiers = prevModifiers;
|
||||
}
|
||||
|
||||
return matches && MatchesKeyboard();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the required key is pressed or no keys are pressed when <see cref="Key"/> is <see cref="Key.None"/>.
|
||||
/// </summary>
|
||||
private bool MatchesKeyboard()
|
||||
{
|
||||
if (Key is Key.None)
|
||||
{
|
||||
return !IsAnyKeyPressed();
|
||||
}
|
||||
|
||||
return Keyboard.IsKeyDown(Key);
|
||||
}
|
||||
|
||||
private static readonly Key[] _allKeys = new[]
|
||||
{
|
||||
// Alphanumeric
|
||||
Key.A, Key.B, Key.C, Key.D, Key.E, Key.F, Key.G, Key.H, Key.I, Key.J,
|
||||
Key.K, Key.L, Key.M, Key.N, Key.O, Key.P, Key.Q, Key.R, Key.S, Key.T,
|
||||
Key.U, Key.V, Key.W, Key.X, Key.Y, Key.Z,
|
||||
Key.D0, Key.D1, Key.D2, Key.D3, Key.D4, Key.D5, Key.D6, Key.D7, Key.D8, Key.D9,
|
||||
|
||||
// Punctuation and symbols
|
||||
Key.Oem3, Key.OemMinus, Key.OemPlus, Key.OemOpenBrackets, Key.OemCloseBrackets,
|
||||
Key.Oem5, Key.Oem1, Key.OemQuotes, Key.OemComma, Key.OemPeriod, Key.Oem2,
|
||||
|
||||
// Function keys
|
||||
Key.F1, Key.F2, Key.F3, Key.F4, Key.F5, Key.F6, Key.F7, Key.F8,
|
||||
Key.F9, Key.F10, Key.F11, Key.F12,
|
||||
|
||||
// Navigation
|
||||
Key.Left, Key.Right, Key.Up, Key.Down,
|
||||
Key.PageUp, Key.PageDown, Key.Home, Key.End,
|
||||
|
||||
// Editing
|
||||
Key.Back, Key.Delete, Key.Insert,
|
||||
|
||||
// Special
|
||||
Key.Space, Key.Return, Key.Escape, Key.Tab,
|
||||
|
||||
// Numeric keypad
|
||||
Key.NumPad0, Key.NumPad1, Key.NumPad2, Key.NumPad3, Key.NumPad4,
|
||||
Key.NumPad5, Key.NumPad6, Key.NumPad7, Key.NumPad8, Key.NumPad9,
|
||||
Key.Multiply, Key.Add, Key.Subtract, Key.Divide, Key.Decimal
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether any key (excluding modifiers) is currently pressed.
|
||||
/// </summary>
|
||||
private static bool IsAnyKeyPressed()
|
||||
=> _allKeys.Any(Keyboard.IsKeyDown);
|
||||
|
||||
private static bool IsButtonReleased(InputEventArgs e)
|
||||
{
|
||||
if (e is MouseButtonEventArgs mbe && mbe.ButtonState == MouseButtonState.Released)
|
||||
return true;
|
||||
|
||||
if (e is MouseWheelEventArgs mwe && mwe.MiddleButton == MouseButtonState.Released)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
Nodify/Interactivity/Gestures/MultiGesture.cs
Normal file
68
Nodify/Interactivity/Gestures/MultiGesture.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
/// <summary>Combines multiple input gestures.</summary>
|
||||
public class MultiGesture : InputGesture
|
||||
{
|
||||
public static readonly MultiGesture None = new MultiGesture(Match.Any);
|
||||
|
||||
/// <summary>The strategy used by <see cref="Matches(object, InputEventArgs)"/>.</summary>
|
||||
public enum Match
|
||||
{
|
||||
/// <summary>At least one gesture must match.</summary>
|
||||
Any,
|
||||
/// <summary>All gestures must match.</summary>
|
||||
All
|
||||
}
|
||||
|
||||
private readonly InputGesture[] _gestures;
|
||||
private readonly Match _match;
|
||||
|
||||
/// <summary>Constructs an instance of a <see cref="MultiGesture"/>.</summary>
|
||||
/// <param name="match">The matching strategy.</param>
|
||||
/// <param name="gestures">The input gestures.</param>
|
||||
public MultiGesture(Match match, params InputGesture[] gestures)
|
||||
{
|
||||
_gestures = gestures;
|
||||
_match = match;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Matches(object targetElement, InputEventArgs inputEventArgs)
|
||||
{
|
||||
if (_match == Match.Any)
|
||||
{
|
||||
return MatchesAny(targetElement, inputEventArgs);
|
||||
}
|
||||
|
||||
return MatchesAll(targetElement, inputEventArgs);
|
||||
}
|
||||
|
||||
private bool MatchesAll(object targetElement, InputEventArgs inputEventArgs)
|
||||
{
|
||||
for (int i = 0; i < _gestures.Length; i++)
|
||||
{
|
||||
if (!_gestures[i].Matches(targetElement, inputEventArgs))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool MatchesAny(object targetElement, InputEventArgs inputEventArgs)
|
||||
{
|
||||
for (int i = 0; i < _gestures.Length; i++)
|
||||
{
|
||||
if (_gestures[i].Matches(targetElement, inputEventArgs))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Nodify/Interactivity/IInputHandler.cs
Normal file
34
Nodify/Interactivity/IInputHandler.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a contract for handling input events within an element or system.
|
||||
/// </summary>
|
||||
public interface IInputHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles a given input event, such as a mouse or keyboard interaction.
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="InputEventArgs"/> representing the input event.</param>
|
||||
/// <remarks>
|
||||
/// This method is invoked when an input event is dispatched to the handler. Implementations should
|
||||
/// handle the event logic and optionally mark the event as handled.
|
||||
/// </remarks>
|
||||
void HandleEvent(InputEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the handler requires input capture to remain active.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property can be used to determine whether it is safe to release mouse capture, especially during toggled interactions. <br />
|
||||
/// Toggled interactions usually involve two steps, and it is important to keep the input capture active until the interaction is completed.
|
||||
/// </remarks>
|
||||
bool RequiresInputCapture { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether events that have been handled should be processed too.
|
||||
/// </summary>
|
||||
bool ProcessHandledEvents { get; }
|
||||
}
|
||||
}
|
||||
95
Nodify/Interactivity/InputElementState.cs
Normal file
95
Nodify/Interactivity/InputElementState.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a base class for handling input events in a specific state for a framework element.
|
||||
/// </summary>
|
||||
/// <typeparam name="TElement">The type of the framework element that owns this state.</typeparam>
|
||||
public abstract class InputElementState<TElement> : IInputHandler
|
||||
where TElement : FrameworkElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the owner of the state.
|
||||
/// </summary>
|
||||
protected TElement Element { get; }
|
||||
|
||||
public bool RequiresInputCapture { get; protected set; }
|
||||
public bool ProcessHandledEvents { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputElementState{TElement}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="element">The framework element that owns this state.</param>
|
||||
protected InputElementState(TElement element)
|
||||
{
|
||||
Element = element;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="UIElement.OnMouseDown(MouseButtonEventArgs)"/>
|
||||
protected virtual void OnMouseDown(MouseButtonEventArgs e) { }
|
||||
|
||||
/// <inheritdoc cref="UIElement.OnMouseUp(MouseButtonEventArgs)"/>
|
||||
protected virtual void OnMouseUp(MouseButtonEventArgs e) { }
|
||||
|
||||
/// <inheritdoc cref="UIElement.OnMouseMove(MouseEventArgs)"/>
|
||||
protected virtual void OnMouseMove(MouseEventArgs e) { }
|
||||
|
||||
/// <inheritdoc cref="UIElement.OnMouseWheel(MouseWheelEventArgs)"/>
|
||||
protected virtual void OnMouseWheel(MouseWheelEventArgs e) { }
|
||||
|
||||
/// <inheritdoc cref="UIElement.OnKeyUp(KeyEventArgs)"/>
|
||||
protected virtual void OnKeyUp(KeyEventArgs e) { }
|
||||
|
||||
/// <inheritdoc cref="UIElement.OnKeyDown(KeyEventArgs)"/>
|
||||
protected virtual void OnKeyDown(KeyEventArgs e) { }
|
||||
|
||||
/// <inheritdoc cref="UIElement.OnLostMouseCapture(MouseEventArgs)"/>
|
||||
protected virtual void OnLostMouseCapture(MouseEventArgs e) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called for any input event that is not explicitly handled by other methods.
|
||||
/// </summary>
|
||||
/// <param name="e">The input event arguments.</param>
|
||||
protected virtual void OnEvent(InputEventArgs e) { }
|
||||
|
||||
/// <summary>
|
||||
/// Processes the input event by invoking the appropriate handler method based on the routed event.
|
||||
/// </summary>
|
||||
/// <param name="e">The input event arguments.</param>
|
||||
public void HandleEvent(InputEventArgs e)
|
||||
{
|
||||
if (e.RoutedEvent == UIElement.MouseMoveEvent)
|
||||
{
|
||||
OnMouseMove((MouseEventArgs)e);
|
||||
}
|
||||
else if (e.RoutedEvent == UIElement.MouseDownEvent)
|
||||
{
|
||||
OnMouseDown((MouseButtonEventArgs)e);
|
||||
}
|
||||
else if (e.RoutedEvent == UIElement.MouseUpEvent)
|
||||
{
|
||||
OnMouseUp((MouseButtonEventArgs)e);
|
||||
}
|
||||
else if (e.RoutedEvent == UIElement.MouseWheelEvent)
|
||||
{
|
||||
OnMouseWheel((MouseWheelEventArgs)e);
|
||||
}
|
||||
else if (e.RoutedEvent == UIElement.LostMouseCaptureEvent)
|
||||
{
|
||||
OnLostMouseCapture((MouseEventArgs)e);
|
||||
}
|
||||
else if (e.RoutedEvent == UIElement.KeyDownEvent)
|
||||
{
|
||||
OnKeyDown((KeyEventArgs)e);
|
||||
}
|
||||
else if (e.RoutedEvent == UIElement.KeyUpEvent)
|
||||
{
|
||||
OnKeyUp((KeyEventArgs)e);
|
||||
}
|
||||
|
||||
OnEvent(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
Nodify/Interactivity/InputElementStateStack.DragState.cs
Normal file
75
Nodify/Interactivity/InputElementStateStack.DragState.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
public partial class InputElementStateStack<TElement> where TElement : FrameworkElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a specialized state for handling drag interactions.
|
||||
/// </summary>
|
||||
public abstract class DragState : DragState<TElement>, IInputElementState, IInputHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the state stack managing this state.
|
||||
/// </summary>
|
||||
public InputElementStateStack<TElement> Stack { get; }
|
||||
|
||||
private readonly InputEventArgs _mouseEventArgs = new MouseEventArgs(Mouse.PrimaryDevice, 0, Stylus.CurrentStylusDevice)
|
||||
{
|
||||
RoutedEvent = NodifyEditor.ViewportUpdatedEvent // dummy event
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DragState"/> class.
|
||||
/// </summary>
|
||||
/// <param name="stack">The state stack managing this state.</param>
|
||||
/// <param name="exitGesture">The gesture used to exit the drag state.</param>
|
||||
/// <param name="cancelGesture">The gesture used to cancel the drag state.</param>
|
||||
public DragState(InputElementStateStack<TElement> stack, InputGesture exitGesture, InputGesture cancelGesture)
|
||||
: base(stack.Element, exitGesture, cancelGesture)
|
||||
{
|
||||
PositionElement = stack.Element;
|
||||
Stack = stack;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DragState"/> class with an optional cancel gesture.
|
||||
/// </summary>
|
||||
/// <param name="stack">The state stack managing this state.</param>
|
||||
/// <param name="exitGesture">The gesture used to exit the drag state.</param>
|
||||
public DragState(InputElementStateStack<TElement> stack, InputGesture exitGesture)
|
||||
: base(stack.Element, exitGesture)
|
||||
{
|
||||
PositionElement = stack.Element;
|
||||
Stack = stack;
|
||||
}
|
||||
|
||||
public void Enter(IInputElementState? from)
|
||||
=> BeginDrag(_mouseEventArgs);
|
||||
|
||||
public void Exit()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes a new state onto the stack.
|
||||
/// </summary>
|
||||
/// <param name="newState">The new state to push.</param>
|
||||
public void PushState(IInputElementState newState)
|
||||
=> Stack.PushState(newState);
|
||||
|
||||
/// <summary>
|
||||
/// Pops the current state from the stack.
|
||||
/// </summary>
|
||||
public void PopState()
|
||||
=> Stack.PopState();
|
||||
|
||||
protected override void OnCancel(InputEventArgs e)
|
||||
=> PopState();
|
||||
|
||||
protected override void OnEnd(InputEventArgs e)
|
||||
=> PopState();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
public partial class InputElementStateStack<TElement> where TElement : FrameworkElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for defining input element states.
|
||||
/// </summary>
|
||||
public abstract class InputElementState : InputElementState<TElement>, IInputElementState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the state stack managing this state.
|
||||
/// </summary>
|
||||
protected InputElementStateStack<TElement> Stack { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputElementState"/> class.
|
||||
/// </summary>
|
||||
/// <param name="stack">The state stack managing this state.</param>
|
||||
public InputElementState(InputElementStateStack<TElement> stack) : base(stack.Element)
|
||||
{
|
||||
Stack = stack;
|
||||
}
|
||||
|
||||
public virtual void Enter(IInputElementState? from) { }
|
||||
|
||||
public virtual void Exit() { }
|
||||
|
||||
/// <summary>
|
||||
/// Pushes a new state onto the stack.
|
||||
/// </summary>
|
||||
/// <param name="newState">The new state to push.</param>
|
||||
public void PushState(IInputElementState newState)
|
||||
=> Stack.PushState(newState);
|
||||
|
||||
/// <summary>
|
||||
/// Pops the current state from the stack.
|
||||
/// </summary>
|
||||
public void PopState()
|
||||
=> Stack.PopState();
|
||||
}
|
||||
}
|
||||
}
|
||||
104
Nodify/Interactivity/InputElementStateStack.cs
Normal file
104
Nodify/Interactivity/InputElementStateStack.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages a stack of input states for a UI element, enabling complex input interactions.
|
||||
/// </summary>
|
||||
/// <typeparam name="TElement">The type of the associated FrameworkElement.</typeparam>
|
||||
public partial class InputElementStateStack<TElement> : IInputHandler
|
||||
where TElement : FrameworkElement
|
||||
{
|
||||
private readonly Stack<IInputElementState> _states = new Stack<IInputElementState>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated element for which this state stack is managing input states.
|
||||
/// </summary>
|
||||
protected TElement Element { get; }
|
||||
|
||||
public bool RequiresInputCapture => State.RequiresInputCapture;
|
||||
|
||||
public bool ProcessHandledEvents => State.ProcessHandledEvents;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputElementStateStack{TElement}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="element">The element associated with this state stack.</param>
|
||||
public InputElementStateStack(TElement element)
|
||||
{
|
||||
Element = element;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current state at the top of the stack.
|
||||
/// </summary>
|
||||
public IInputElementState State => _states.Peek();
|
||||
|
||||
/// <summary>Pushes a new state into the stack.</summary>
|
||||
/// <param name="newState">The new state.</param>
|
||||
/// <remarks>Calls <see cref="IInputElementState.Enter"/> on the new state.</remarks>
|
||||
public void PushState(IInputElementState newState)
|
||||
{
|
||||
var prev = _states.Count > 0 ? State : null;
|
||||
_states.Push(newState);
|
||||
newState.Enter(prev);
|
||||
}
|
||||
|
||||
/// <summary>Pops the current state from the stack.</summary>
|
||||
/// <remarks>It doesn't pop the initial state.
|
||||
/// <br />Calls <see cref="IInputElementState.Exit"/> on the current state.
|
||||
/// <br />Calls <see cref="IInputElementState.Enter"/> on the new state.</remarks>
|
||||
public void PopState()
|
||||
{
|
||||
// Never remove the default state
|
||||
if (_states.Count > 1)
|
||||
{
|
||||
IInputElementState prev = _states.Pop();
|
||||
prev.Exit();
|
||||
State.Enter(prev);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Pops all states from the stack.</summary>
|
||||
/// <remarks>It doesn't pop the initial state.
|
||||
/// <br />Calls <see cref="IInputElementState.Exit"/> on the current state.
|
||||
/// <br />Calls <see cref="IInputElementState.Enter"/> on the previous state.
|
||||
/// </remarks>
|
||||
public void PopAllStates()
|
||||
{
|
||||
while (_states.Count > 1)
|
||||
{
|
||||
PopState();
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleEvent(InputEventArgs e)
|
||||
{
|
||||
State.HandleEvent(e);
|
||||
|
||||
if (e.RoutedEvent == UIElement.LostMouseCaptureEvent)
|
||||
{
|
||||
PopAllStates();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing a state in the input state stack.
|
||||
/// </summary>
|
||||
public interface IInputElementState : IInputHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when entering this state from another state.
|
||||
/// </summary>
|
||||
/// <param name="from">The state being exited, or null if entering from no prior state.</param>
|
||||
void Enter(IInputElementState? from);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when exiting this state.
|
||||
/// </summary>
|
||||
void Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
127
Nodify/Interactivity/InputProcessor.Shared.cs
Normal file
127
Nodify/Interactivity/InputProcessor.Shared.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
public partial class InputProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// A shared input processor that allows registering and managing global input handlers for a specific type of UI element.
|
||||
/// </summary>
|
||||
/// <typeparam name="TElement">The type of the UI element that the input handlers will be associated with.</typeparam>
|
||||
public sealed class Shared<TElement> : InputProcessor, IInputHandler
|
||||
where TElement : FrameworkElement
|
||||
{
|
||||
private static readonly List<KeyValuePair<Type, Func<TElement, IInputHandler>>> _handlerFactories = new List<KeyValuePair<Type, Func<TElement, IInputHandler>>>();
|
||||
|
||||
bool IInputHandler.ProcessHandledEvents { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Shared{TElement}"/> class for the specified UI element.
|
||||
/// </summary>
|
||||
/// <param name="element">The UI element that the shared input handlers will be associated with.</param>
|
||||
public Shared(TElement element)
|
||||
{
|
||||
foreach (var kvp in _handlerFactories)
|
||||
{
|
||||
AddHandler(kvp.Value(element));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes static members of the <see cref="Shared{TElement}"/> class.
|
||||
/// Ensures default handlers are registered before methods on this class can be used.
|
||||
/// </summary>
|
||||
static Shared()
|
||||
{
|
||||
if (typeof(TElement) == typeof(NodifyEditor))
|
||||
{
|
||||
EditorState.RegisterDefaultHandlers();
|
||||
}
|
||||
else if (typeof(TElement) == typeof(ItemContainer))
|
||||
{
|
||||
ContainerState.RegisterDefaultHandlers();
|
||||
}
|
||||
else if (typeof(TElement) == typeof(Connector))
|
||||
{
|
||||
ConnectorState.RegisterDefaultHandlers();
|
||||
}
|
||||
else if (typeof(TElement) == typeof(Minimap))
|
||||
{
|
||||
MinimapState.RegisterDefaultHandlers();
|
||||
}
|
||||
else if (typeof(TElement) == typeof(BaseConnection))
|
||||
{
|
||||
ConnectionState.RegisterDefaultHandlers();
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleEvent(InputEventArgs e)
|
||||
=> ProcessEvent(e);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a factory method for creating an input handler of the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="THandler">The type of the input handler to register.</typeparam>
|
||||
/// <param name="factory">A factory method that creates an instance of the input handler for a given UI element.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown if an input handler of the specified type is already registered.
|
||||
/// </exception>
|
||||
public static void RegisterHandlerFactory<THandler>(Func<TElement, THandler> factory)
|
||||
where THandler : IInputHandler
|
||||
{
|
||||
if (_handlerFactories.Any(x => x.Key == typeof(THandler)))
|
||||
{
|
||||
throw new InvalidOperationException($"An input handler of type '{typeof(THandler)}' has already been registered.");
|
||||
}
|
||||
|
||||
_handlerFactories.Add(new KeyValuePair<Type, Func<TElement, IInputHandler>>(typeof(THandler), elem => factory(elem)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the registered factory method for creating input handlers of the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="THandler">The type of the input handler to remove.</typeparam>
|
||||
public static void RemoveHandlerFactory<THandler>()
|
||||
=> _handlerFactories.RemoveAll(x => x.Key == typeof(THandler));
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the registered factory method with another one of the same type.
|
||||
/// </summary>
|
||||
/// <typeparam name="THandler">The type of the input handler to replace.</typeparam>
|
||||
public static void ReplaceHandlerFactory<THandler>(Func<TElement, THandler> factory)
|
||||
where THandler : IInputHandler
|
||||
{
|
||||
int index = _handlerFactories.FindIndex(x => x.Key == typeof(THandler));
|
||||
_handlerFactories[index] = new KeyValuePair<Type, Func<TElement, IInputHandler>>(typeof(THandler), elem => factory(elem));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all registered handler factories, effectively removing all shared input handlers.
|
||||
/// </summary>
|
||||
public static void ClearHandlerFactories()
|
||||
=> _handlerFactories.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for the <see cref="InputProcessor"/> class.
|
||||
/// </summary>
|
||||
public static class InputProcessorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds shared input handlers for the specified UI element instance to the input processor.
|
||||
/// </summary>
|
||||
/// <typeparam name="TElement">The type of the UI element for which shared input handlers are being added.</typeparam>
|
||||
/// <param name="inputProcessor">The input processor to which the shared handlers will be added.</param>
|
||||
/// <param name="instance">The UI element instance associated with the shared handlers.</param>
|
||||
public static void AddSharedHandlers<TElement>(this InputProcessor inputProcessor, TElement instance)
|
||||
where TElement : FrameworkElement
|
||||
{
|
||||
inputProcessor.AddHandler(new InputProcessor.Shared<TElement>(instance));
|
||||
}
|
||||
}
|
||||
}
|
||||
61
Nodify/Interactivity/InputProcessor.cs
Normal file
61
Nodify/Interactivity/InputProcessor.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes input events and delegates them to registered handlers.
|
||||
/// </summary>
|
||||
public partial class InputProcessor
|
||||
{
|
||||
private readonly List<IInputHandler> _handlers = new List<IInputHandler>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the processor has ongoing interactions that require input capture to remain active.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property can be used to determine whether it is safe to release mouse capture, especially during toggled interactions. <br />
|
||||
/// Toggled interactions usually involve two steps, and it is important to keep the input capture active until the interaction is completed.
|
||||
/// </remarks>
|
||||
public bool RequiresInputCapture { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds an input handler to the processor.
|
||||
/// </summary>
|
||||
/// <param name="handler">The input handler to add.</param>
|
||||
public void AddHandler(IInputHandler handler)
|
||||
=> _handlers.Add(handler);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all handlers of the specified type from the processor.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the handler to remove.</typeparam>
|
||||
public void RemoveHandlers<T>() where T : IInputHandler
|
||||
=> _handlers.RemoveAll(x => x.GetType() == typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Clears all registered handlers.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
=> _handlers.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Processes an input event and delegates it to the registered handlers.
|
||||
/// </summary>
|
||||
/// <param name="e">The input event arguments to process.</param>
|
||||
public void ProcessEvent(InputEventArgs e)
|
||||
{
|
||||
RequiresInputCapture = false;
|
||||
|
||||
for (int i = 0; i < _handlers.Count; i++)
|
||||
{
|
||||
IInputHandler handler = _handlers[i];
|
||||
if (!e.Handled || handler.ProcessHandledEvents)
|
||||
{
|
||||
handler.HandleEvent(e);
|
||||
RequiresInputCapture |= handler.RequiresInputCapture;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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