From 0317dc8f28a7130d57a969b5a36d666c6f265cb9 Mon Sep 17 00:00:00 2001 From: miroiu Date: Sat, 7 Jun 2025 23:58:58 +0300 Subject: [PATCH 01/28] Added support for keyboard navigation layers --- Examples/Nodify.Playground/App.xaml | 14 ++ Examples/Nodify.Playground/MainWindow.xaml.cs | 12 ++ Examples/Nodify.Shapes/App.xaml.cs | 1 + Nodify/Containers/ItemContainer.cs | 21 +- .../Editor/NodifyEditor.KeyboardNavigation.cs | 179 ++++++++++++++++++ Nodify/Editor/NodifyEditor.cs | 60 ++++++ Nodify/Editor/States/EditorState.cs | 1 + Nodify/Editor/States/KeyboardNavigation.cs | 83 ++++++++ .../Interactivity/Gestures/EditorGestures.cs | 151 +++++++++++++++ .../Interactivity/Gestures/KeyComboGesture.cs | 108 +++++++++++ Nodify/Interactivity/INavigationLayer.cs | 33 ++++ Nodify/Interactivity/InputElementState.cs | 3 +- Nodify/Nodes/Node.cs | 5 + Nodify/Themes/Styles/Node.xaml | 8 +- Nodify/Utilities/EditorGesturesExtensions.cs | 67 +++++++ Nodify/Utilities/SelectionHelper.cs | 23 --- Nodify/Utilities/WeakReferenceCollection.cs | 132 +++++++++++++ README.md | 4 +- 18 files changed, 875 insertions(+), 30 deletions(-) create mode 100644 Nodify/Editor/NodifyEditor.KeyboardNavigation.cs create mode 100644 Nodify/Editor/States/KeyboardNavigation.cs create mode 100644 Nodify/Interactivity/Gestures/KeyComboGesture.cs create mode 100644 Nodify/Interactivity/INavigationLayer.cs create mode 100644 Nodify/Utilities/EditorGesturesExtensions.cs create mode 100644 Nodify/Utilities/WeakReferenceCollection.cs diff --git a/Examples/Nodify.Playground/App.xaml b/Examples/Nodify.Playground/App.xaml index c6929cbe..b0409fcf 100644 --- a/Examples/Nodify.Playground/App.xaml +++ b/Examples/Nodify.Playground/App.xaml @@ -9,6 +9,20 @@ + + + diff --git a/Examples/Nodify.Playground/MainWindow.xaml.cs b/Examples/Nodify.Playground/MainWindow.xaml.cs index feef5652..565a1df0 100644 --- a/Examples/Nodify.Playground/MainWindow.xaml.cs +++ b/Examples/Nodify.Playground/MainWindow.xaml.cs @@ -1,5 +1,7 @@ using System; +using System.Diagnostics; using System.Windows; +using System.Windows.Input; using System.Windows.Media; namespace Nodify.Playground @@ -54,6 +56,16 @@ public MainWindow() InitializeComponent(); CompositionTargetEx.Rendering += OnRendering; + + EventManager.RegisterClassHandler( + typeof(UIElement), + Keyboard.PreviewGotKeyboardFocusEvent, + (KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus); + } + + private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) + { + Title = e.NewFocus.ToString(); } private void OnRendering(double fps) diff --git a/Examples/Nodify.Shapes/App.xaml.cs b/Examples/Nodify.Shapes/App.xaml.cs index bff9a081..5b0e8d6b 100644 --- a/Examples/Nodify.Shapes/App.xaml.cs +++ b/Examples/Nodify.Shapes/App.xaml.cs @@ -15,6 +15,7 @@ public App() NodifyEditor.EnableCuttingLinePreview = true; EditorGestures.Mappings.Connection.Disconnect.Value = new AnyGesture(new Interactivity.MouseGesture(MouseAction.LeftClick, ModifierKeys.Alt), new Interactivity.MouseGesture(MouseAction.RightClick)); + EditorGestures.Mappings.Editor.Pan.Value = new AnyGesture(EditorGestures.Mappings.Editor.Pan.Value, new Interactivity.MouseGesture(MouseAction.LeftClick, Key.Space)); } } } diff --git a/Nodify/Containers/ItemContainer.cs b/Nodify/Containers/ItemContainer.cs index 831ce8ee..aa3d76c5 100644 --- a/Nodify/Containers/ItemContainer.cs +++ b/Nodify/Containers/ItemContainer.cs @@ -233,6 +233,11 @@ private static void OnIsSelectedChanged(DependencyObject d, DependencyPropertyCh /// Has no effect if the container has a context menu. public static bool PreserveSelectionOnRightClick { get; set; } + /// + /// Gets or sets the default viewport edge offset applied when bringing an item into view as a result of keyboard focus. + /// + public static double BringIntoViewEdgeOffset { get; set; } = 32d; + /// /// The that owns this . /// @@ -247,6 +252,11 @@ private static void OnIsSelectedChanged(DependencyObject d, DependencyPropertyCh BorderThickness.Right - SelectedBorderThickness.Right, BorderThickness.Bottom - SelectedBorderThickness.Bottom); + /// + /// Gets the bounds of the selection area for this based on its and . + /// + public Rect Bounds => new Rect(Location, DesiredSizeForSelection ?? RenderSize); + #endregion /// @@ -268,6 +278,11 @@ static ItemContainer() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(typeof(ItemContainer))); FocusableProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(BoxValue.True)); + + //KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(KeyboardNavigationMode.Local)); + //KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(KeyboardNavigationMode.Contained)); + //FocusManager.IsFocusScopeProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(BoxValue.True)); + } /// @@ -295,7 +310,7 @@ protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) /// True if is selectable. protected internal virtual bool IsSelectableLocation(Point position) { - Size size = DesiredSizeForSelection ?? RenderSize; + Size size = Bounds.Size; return position.X >= 0 && position.Y >= 0 && position.X <= size.Width && position.Y <= size.Height; } @@ -307,7 +322,7 @@ protected internal virtual bool IsSelectableLocation(Point position) /// True if contains or intersects this . public virtual bool IsSelectableInArea(Rect area, bool isContained) { - var bounds = new Rect(Location, DesiredSizeForSelection ?? RenderSize); + var bounds = Bounds; return isContained ? area.Contains(bounds) : area.IntersectsWith(bounds); } @@ -370,7 +385,7 @@ protected override void OnMouseDown(MouseButtonEventArgs e) protected override void OnMouseUp(MouseButtonEventArgs e) { InputProcessor.ProcessEvent(e); - + // Release the mouse capture if all the mouse buttons are released and there's no interaction in progress if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released) { diff --git a/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs b/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs new file mode 100644 index 00000000..516c5047 --- /dev/null +++ b/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs @@ -0,0 +1,179 @@ +using Nodify.Interactivity; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Input; +using System.Windows; +using System.Collections; + +namespace Nodify +{ + public partial class NodifyEditor : IKeyboardNavigationLayer, INavigationLayerGroup + { + private readonly List _focusScopes = new List(); + private IKeyboardNavigationLayer _activeFocusScope; + + public IKeyboardNavigationLayer ActiveLayer => _activeFocusScope; + + bool IKeyboardNavigationLayer.IsActiveLayer => ActiveLayer == this; + + KeyboardNavigationLayerId IKeyboardNavigationLayer.Id => KeyboardNavigationLayerId.Nodes; + + int IReadOnlyCollection.Count => _focusScopes.Count; + + // TODO: When do we clear these? + + #region Focus Handling + + bool IKeyboardNavigationLayer.TryMoveFocus(TraversalRequest request) + { + if (TryGetContainerToFocus(out var containerToFocus, request)) + { + containerToFocus!.Focus(); + BringIntoView(containerToFocus, ItemContainer.BringIntoViewEdgeOffset); + return true; + } + + return false; + } + + private bool TryGetContainerToFocus(out ItemContainer? containerToFocus, TraversalRequest request) + { + containerToFocus = null; + + if (Keyboard.FocusedElement is ItemContainer focusedContainer) + { + containerToFocus = GetDirectionalFocusTarget(focusedContainer, request); + } + else if (Keyboard.FocusedElement is NodifyEditor editor && editor.ItemContainers.Count > 0) + { + var viewport = new Rect(ViewportLocation, ViewportSize); + containerToFocus = ItemContainers.FirstOrDefault(container => container.IsSelectableInArea(viewport, isContained: false)) + ?? ItemContainers.FirstOrDefault(); // TODO: Find the left most one? + } + else if (Keyboard.FocusedElement is UIElement elem && elem.GetParentOfType() is ItemContainer parentContainer) + { + containerToFocus = parentContainer; + } + + return containerToFocus != null; + } + + // TODO: Customizable + protected virtual ItemContainer? GetDirectionalFocusTarget(ItemContainer currentContainer, TraversalRequest request) + { + var currentContainerBounds = new Rect(currentContainer.Location, currentContainer.DesiredSizeForSelection ?? currentContainer.RenderSize); + var currentContainerCenter = new Point(currentContainerBounds.X + currentContainerBounds.Width / 2, currentContainerBounds.Y + currentContainerBounds.Height / 2); + + //IEnumerable candidates = request.FocusNavigationDirection switch + //{ + // FocusNavigationDirection.Left => ItemContainers.Where(c => + // { + // var center = new Point(c.Bounds.X + c.Bounds.Width / 2, c.Bounds.Y + c.Bounds.Height / 2); + // return center.X < currentContainerCenter.X; + // }), + // FocusNavigationDirection.Right => ItemContainers.Where(c => + // { + // var center = new Point(c.Bounds.X + c.Bounds.Width / 2, c.Bounds.Y + c.Bounds.Height / 2); + // return center.X > currentContainerCenter.X; + // }), + // FocusNavigationDirection.Up => ItemContainers.Where(c => + // { + // var center = new Point(c.Bounds.X + c.Bounds.Width / 2, c.Bounds.Y + c.Bounds.Height / 2); + // return center.Y < currentContainerCenter.Y; + // }), + // FocusNavigationDirection.Down => ItemContainers.Where(c => + // { + // var center = new Point(c.Bounds.X + c.Bounds.Width / 2, c.Bounds.Y + c.Bounds.Height / 2); + // return center.Y > currentContainerCenter.Y; + // }), + // _ => Enumerable.Empty() + //}; + + var itemContainers = ItemContainers; + + IEnumerable candidates = request.FocusNavigationDirection switch + { + FocusNavigationDirection.Left => itemContainers.Where(c => c.Bounds.Right <= currentContainerBounds.Left), + FocusNavigationDirection.Right => itemContainers.Where(c => c.Location.X >= currentContainerBounds.Right), + FocusNavigationDirection.Up => itemContainers.Where(c => c.Bounds.Bottom <= currentContainerBounds.Top), + FocusNavigationDirection.Down => itemContainers.Where(c => c.Location.Y >= currentContainerBounds.Bottom), + _ => Enumerable.Empty() + }; + + // Wrap focus if no candidates found in the current direction + if (!candidates.Any()) + { + candidates = request.FocusNavigationDirection switch + { + FocusNavigationDirection.Left => itemContainers.OrderByDescending(c => c.Location.X).Take(1), + FocusNavigationDirection.Right => itemContainers.OrderBy(c => c.Location.X).Take(1), + FocusNavigationDirection.Up => itemContainers.OrderByDescending(c => c.Location.Y).Take(1), + FocusNavigationDirection.Down => itemContainers.OrderBy(c => c.Location.Y).Take(1), + _ => Enumerable.Empty() + }; + + request.Wrapped = true; + } + + ItemContainer? best = null; + double minDistanceSquared = double.MaxValue; + + foreach (var candidate in candidates) + { + var bounds = new Rect(candidate.Location, candidate.DesiredSizeForSelection ?? candidate.RenderSize); + var center = new Point(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2); + + double distanceSquared = (center - currentContainerCenter).LengthSquared; + if (distanceSquared < minDistanceSquared) + { + minDistanceSquared = distanceSquared; + best = candidate; + } + } + + return best; + } + + public bool MoveFocus(FocusNavigationDirection direction) + => MoveFocus(new TraversalRequest(direction)); + + public new bool MoveFocus(TraversalRequest request) + => ActiveLayer.TryMoveFocus(request); + + void INavigationLayerGroup.RegisterLayer(IKeyboardNavigationLayer layer) + { + _focusScopes.Add(layer); + } + + void INavigationLayerGroup.RemoveLayer(KeyboardNavigationLayerId layerId) + { + _focusScopes.Remove(_focusScopes.FirstOrDefault(layer => layer.Id == layerId)!); + } + + void INavigationLayerGroup.MoveToNextLayer() + { + int currentIndex = _focusScopes.IndexOf(ActiveLayer); + if (currentIndex >= 0 && currentIndex < _focusScopes.Count - 1) + { + _activeFocusScope = _focusScopes[currentIndex + 1]; + // TODO: Focus container or logical element? + _activeFocusScope.TryMoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); + } + } + + void INavigationLayerGroup.MoveToPrevLayer() + { + throw new NotImplementedException(); + } + + public IEnumerator GetEnumerator() + => _focusScopes.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + #endregion + + } +} diff --git a/Nodify/Editor/NodifyEditor.cs b/Nodify/Editor/NodifyEditor.cs index 0c1d50b8..4a1d12df 100644 --- a/Nodify/Editor/NodifyEditor.cs +++ b/Nodify/Editor/NodifyEditor.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -552,6 +553,10 @@ static NodifyEditor() DefaultStyleKeyProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(typeof(NodifyEditor))); FocusableProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.True)); + KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(KeyboardNavigationMode.None)); + KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(KeyboardNavigationMode.None)); + FocusManager.IsFocusScopeProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.True)); + EditorCommands.RegisterCommandBindings(); } @@ -575,6 +580,9 @@ public NodifyEditor() InputProcessor.AddSharedHandlers(this); Unloaded += OnEditorUnloaded; + + _focusScopes.Add(this); + _activeFocusScope = this; } /// @@ -689,6 +697,52 @@ public void BringIntoView(Point point, bool animated = true, Action? onFinish = public new void BringIntoView(Rect area) => BringIntoView(new Point(area.X + area.Width / 2, area.Y + area.Height / 2)); + /// + /// Ensures the specified item container is fully visible within the viewport, optionally with padding around the edges. + /// + /// The item container to bring into view. + /// The padding to apply around the container + public void BringIntoView(ItemContainer container, double offsetFromEdge = 32d) + { + var viewport = new Rect(ViewportLocation, ViewportSize); + var containerBounds = new Rect(container.Location, container.RenderSize); + + containerBounds.Inflate(offsetFromEdge, offsetFromEdge); + + if (!viewport.Contains(containerBounds)) + { + if (viewport.IntersectsWith(containerBounds)) + { + double newX = viewport.X; + double newY = viewport.Y; + + if (containerBounds.Left < viewport.Left) + { + newX = containerBounds.Left; + } + else if (containerBounds.Right > viewport.Right) + { + newX = containerBounds.Right - viewport.Width; + } + + if (containerBounds.Top < viewport.Top) + { + newY = containerBounds.Top; + } + else if (containerBounds.Bottom > viewport.Bottom) + { + newY = containerBounds.Bottom - viewport.Height; + } + + BringIntoView(new Point(newX, newY) + new Vector(viewport.Width / 2, viewport.Height / 2)); + } + else + { + BringIntoView(containerBounds); + } + } + } + /// /// Scales the viewport to fit the specified or all the s if that's possible. /// @@ -878,6 +932,12 @@ protected override void OnKeyUp(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e) => InputProcessor.ProcessEvent(e); + protected override void OnPreviewKeyDown(KeyEventArgs e) + => InputProcessor.ProcessEvent(e); + + protected override void OnPreviewKeyUp(KeyEventArgs e) + => InputProcessor.ProcessEvent(e); + #endregion /// diff --git a/Nodify/Editor/States/EditorState.cs b/Nodify/Editor/States/EditorState.cs index b2f0bc03..7ae4d5bd 100644 --- a/Nodify/Editor/States/EditorState.cs +++ b/Nodify/Editor/States/EditorState.cs @@ -65,6 +65,7 @@ internal static void RegisterDefaultHandlers() InputProcessor.Shared.RegisterHandlerFactory(elem => new Zooming(elem)); InputProcessor.Shared.RegisterHandlerFactory(elem => new PushingItems(elem)); InputProcessor.Shared.RegisterHandlerFactory(elem => new Cutting(elem)); + InputProcessor.Shared.RegisterHandlerFactory(elem => new KeyboardNavigation(elem)); } } } diff --git a/Nodify/Editor/States/KeyboardNavigation.cs b/Nodify/Editor/States/KeyboardNavigation.cs new file mode 100644 index 00000000..21ab8fd7 --- /dev/null +++ b/Nodify/Editor/States/KeyboardNavigation.cs @@ -0,0 +1,83 @@ +using System; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Input; + +namespace Nodify.Interactivity +{ + public static partial class EditorState + { + // TODO: Move focus manually + public class KeyboardNavigation : InputElementState + { + public KeyboardNavigation(NodifyEditor element) : base(element) + { + ProcessHandledEvents = true; + } + + protected override void OnEvent(InputEventArgs e) + { + //if (e.RoutedEvent == UIElement.PreviewKeyDownEvent && e is KeyEventArgs args && IsDirectionalNavigationKey(args.Key)) + //{ + // OnKeyDown(args); + // e.Handled = true; + //} + } + + private static bool IsDirectionalNavigationKey(Key key) + { + return key is Key.Left || key is Key.Right || key is Key.Up || key is Key.Down; + } + + // TODO: If focus is within, do not allow escaping focus trap unless the escape gesture is performed. (some keys like Space or System Keys could try to escape) + protected override void OnKeyDown(KeyEventArgs e) + { + double cellSize = Element.GridCellSize; + var gestures = EditorGestures.Mappings.Editor.Keyboard; + + if (Element.IsKeyboardFocusWithin && IsEditorControl(e.OriginalSource)) + { + // TODO: Check if the Editor.ActiveFocusScope (ActiveNavigationLayer) is the nodes layer + if (gestures.Pan.TryGetNavigationDirection(e, out var panDirection)) + { + var panning = new Vector(-panDirection.X * cellSize, panDirection.Y * cellSize); + Element.UpdatePanning(panning); + e.Handled = true; + } + else if (Element.SelectedContainersCount > 0 && gestures.MoveSelection.TryGetNavigationDirection(e, out var dragDirection)) + { + var dragging = new Vector(dragDirection.X * cellSize, -dragDirection.Y * cellSize); + Element.BeginDragging(); + Element.UpdateDragging(dragging); + Element.EndDragging(); + + // TODO: Find a way to keep the selection in view + + e.Handled = true; + } + else if (gestures.NavigateSelection.TryGetFocusDirection(e, out var direction)) + { + Element.MoveFocus(direction); + e.Handled = true; + } + else if (gestures.NextNavigationLayer.Matches(e.Source, e)) + { + ((INavigationLayerGroup)Element).MoveToNextLayer(); + e.Handled = true; + } + else if (gestures.PrevNavigationLayer.Matches(e.Source, e)) + { + ((INavigationLayerGroup)Element).MoveToPrevLayer(); + e.Handled = true; + } + } + } + + // TODO: Allow for extensibility because connections can be custom + private static bool IsEditorControl(object originalSource) + { + return originalSource is NodifyEditor || originalSource is ItemContainer || originalSource is Connector || originalSource is BaseConnection; + } + } + } +} diff --git a/Nodify/Interactivity/Gestures/EditorGestures.cs b/Nodify/Interactivity/Gestures/EditorGestures.cs index cb60c60d..a68b5a4d 100644 --- a/Nodify/Interactivity/Gestures/EditorGestures.cs +++ b/Nodify/Interactivity/Gestures/EditorGestures.cs @@ -142,13 +142,112 @@ public void Apply(ItemContainerGestures gestures) Drag.Value = gestures.Drag.Value; CancelAction.Value = gestures.CancelAction.Value; } + + // TODO: Comment + public void Unbind() + { + Selection.Unbind(); + Drag.Unbind(); + CancelAction.Unbind(); + } + } + + // TODO: Comments + 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 }; + } + + public InputGestureRef Up { get; } + public InputGestureRef Left { get; } + public InputGestureRef Down { get; } + public InputGestureRef Right { get; } + + /// Copies from the specified gestures. + /// The gestures to copy. + 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; + } + + public void Unbind() + { + Up.Unbind(); + Left.Unbind(); + Down.Unbind(); + Right.Unbind(); + } } /// Gestures for the editor. public class NodifyEditorGestures { + // TODO: Comments + public class KeyboardNavigation + { + public KeyboardNavigation() + { + Pan = new DirectionalNavigationGestures(Key.Space, repeated: true); + MoveSelection = new DirectionalNavigationGestures(ModifierKeys.Control); + NavigateSelection = new DirectionalNavigationGestures(ModifierKeys.None); + NextNavigationLayer = new KeyGesture(Key.OemCloseBrackets, ModifierKeys.Control); + PrevNavigationLayer = new KeyGesture(Key.OemOpenBrackets, ModifierKeys.Control); + } + + // TODO: Pan large step gesture? + public DirectionalNavigationGestures Pan { get; } + + public DirectionalNavigationGestures MoveSelection { get; } + + public DirectionalNavigationGestures NavigateSelection { get; } + + public InputGestureRef NextNavigationLayer { get; } + public InputGestureRef PrevNavigationLayer { get; } + + public void Apply(KeyboardNavigation gestures) + { + Pan.Apply(gestures.Pan); + NextNavigationLayer.Value = gestures.NextNavigationLayer.Value; + PrevNavigationLayer.Value = gestures.PrevNavigationLayer.Value; + } + + public void Unbind() + { + Pan.Unbind(); + NextNavigationLayer.Unbind(); + PrevNavigationLayer.Unbind(); + } + } + // TODO: + // Pan editor: Space+Arrow keys + // + // Navigate connections = Arrow keys + // Navigate nodes = Arrow keys + // Navigate connectors inside panel = Arrow keys + // Toggle selection = CTRL+Space or Enter + // Move nodes: - CTRL + Arrow Keys – nudge selected node(s) by 1 unit + // - Shift + CTRL + Arrow Keys – nudge by 10 units + // Deselect all = Escape + public NodifyEditorGestures() { + Keyboard = new KeyboardNavigation(); Selection = new SelectionGestures(); SelectAll = ApplicationCommands.SelectAll.InputGestures[0].AsRef(); Cutting = new MouseGesture(MouseAction.LeftClick, ModifierKeys.Alt | ModifierKeys.Shift, true); @@ -165,6 +264,9 @@ public NodifyEditorGestures() PanVerticalModifierKey = ModifierKeys.None; } + // TODO: Comment and rename to Navigation? + public KeyboardNavigation Keyboard { get; } + /// Gesture used to start selecting using a strategy. public SelectionGestures Selection { get; } @@ -222,6 +324,7 @@ public NodifyEditorGestures() /// The gestures to copy. public void Apply(NodifyEditorGestures gestures) { + Keyboard.Apply(gestures.Keyboard); Selection.Apply(gestures.Selection); SelectAll.Value = gestures.SelectAll.Value; Cutting.Value = gestures.Cutting.Value; @@ -237,6 +340,22 @@ public void Apply(NodifyEditorGestures gestures) PanHorizontalModifierKey = gestures.PanHorizontalModifierKey; PanVerticalModifierKey = gestures.PanVerticalModifierKey; } + + // TODO: Comment + public void Unbind() + { + Keyboard.Unbind(); + Selection.Unbind(); + SelectAll.Unbind(); + Cutting.Unbind(); + Pan.Unbind(); + PushItems.Unbind(); + ZoomIn.Unbind(); + ZoomOut.Unbind(); + ResetViewportLocation.Unbind(); + FitToScreen.Unbind(); + CancelAction.Unbind(); + } } /// Gestures used by the . @@ -269,6 +388,14 @@ public void Apply(ConnectorGestures gestures) Connect.Value = gestures.Connect.Value; CancelAction.Value = gestures.CancelAction.Value; } + + // TODO: Comment + public void Unbind() + { + Disconnect.Unbind(); + Connect.Unbind(); + CancelAction.Unbind(); + } } /// Gestures used by the . @@ -300,6 +427,13 @@ public void Apply(ConnectionGestures gestures) Disconnect.Value = gestures.Disconnect.Value; Selection.Apply(gestures.Selection); } + + public void Unbind() + { + Split.Unbind(); + Selection.Unbind(); + Disconnect.Unbind(); + } } /// Gestures for the . @@ -348,6 +482,13 @@ public void Apply(MinimapGestures gestures) CancelAction.Value = gestures.CancelAction.Value; ZoomModifierKey = gestures.ZoomModifierKey; } + + // TODO: Comment + public void Unbind() + { + DragViewport.Unbind(); + CancelAction.Unbind(); + } } /// Gestures for the editor. @@ -379,5 +520,15 @@ public void Apply(EditorGestures gestures) GroupingNode.Apply(gestures.GroupingNode); Minimap.Apply(gestures.Minimap); } + + // TODO: Comment + public void Unbind() + { + Editor.Unbind(); + ItemContainer.Unbind(); + Connector.Unbind(); + Connection.Unbind(); + Minimap.Unbind(); + } } } diff --git a/Nodify/Interactivity/Gestures/KeyComboGesture.cs b/Nodify/Interactivity/Gestures/KeyComboGesture.cs new file mode 100644 index 00000000..6dffb628 --- /dev/null +++ b/Nodify/Interactivity/Gestures/KeyComboGesture.cs @@ -0,0 +1,108 @@ +using System.Windows; +using System.Windows.Input; + +namespace Nodify.Interactivity +{ + /// + /// 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. + /// + internal class KeyComboGesture : KeyGesture + { + private static readonly WeakReferenceCollection _allCombos = new WeakReferenceCollection(16); + + private bool _isTriggerDown; + + /// + /// Gets or sets the key that must be pressed first to activate this combo gesture. + /// + public Key TriggerKey { get; set; } + + /// + /// Gets or sets a value indicating whether the combo key can be repeatedly triggered + /// without releasing the trigger key. + /// + 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); + } + + /// + /// Initializes a new instance of the class with the specified trigger and combo keys. + /// + /// The key that must be pressed first. + /// The combo key pressed while the trigger key is held. + public KeyComboGesture(Key triggerKey, Key comboKey) : this(triggerKey, comboKey, ModifierKeys.None, string.Empty) + { + } + + /// + /// Initializes a new instance of the class with the specified trigger and combo keys and modifiers. + /// + /// The key that must be pressed first. + /// The combo key pressed while the trigger key is held. + /// Any modifier keys required for the combo key. + public KeyComboGesture(Key triggerKey, Key comboKey, ModifierKeys modifiers) : this(triggerKey, comboKey, modifiers, string.Empty) + { + } + + /// + /// Initializes a new instance of the class with the specified trigger key, + /// combo key, modifiers, and display string. + /// + /// The key that must be pressed first. + /// The combo key pressed while the trigger key is held. + /// Any modifier keys required for the combo key. + /// The display string representing the gesture. + 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._isTriggerDown = false; + } + } + + private static void HandleKeyUp(object sender, KeyEventArgs e) + { + foreach (var combo in _allCombos) + { + if (combo._isTriggerDown && e.Key == combo.TriggerKey) + { + combo._isTriggerDown = false; + } + } + } + + 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 && !AllowRepeatingComboKey) + { + _isTriggerDown = false; + } + + return matches; + } + + return false; + } + } +} diff --git a/Nodify/Interactivity/INavigationLayer.cs b/Nodify/Interactivity/INavigationLayer.cs new file mode 100644 index 00000000..5bc6c2cb --- /dev/null +++ b/Nodify/Interactivity/INavigationLayer.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Windows.Input; + +namespace Nodify.Interactivity +{ + public class KeyboardNavigationLayerId + { + public static readonly KeyboardNavigationLayerId Nodes = new KeyboardNavigationLayerId(); + public static readonly KeyboardNavigationLayerId Connections = new KeyboardNavigationLayerId(); + } + + public interface INavigationLayerGroup : IReadOnlyCollection + { + IKeyboardNavigationLayer ActiveLayer { get; } + + void MoveToNextLayer(); + + void MoveToPrevLayer(); + + void RegisterLayer(IKeyboardNavigationLayer layer); + + void RemoveLayer(KeyboardNavigationLayerId layerId); + } + + public interface IKeyboardNavigationLayer + { + KeyboardNavigationLayerId Id { get; } + + bool TryMoveFocus(TraversalRequest request); + + bool IsActiveLayer { get; } + } +} diff --git a/Nodify/Interactivity/InputElementState.cs b/Nodify/Interactivity/InputElementState.cs index 4c63b998..c6e4400f 100644 --- a/Nodify/Interactivity/InputElementState.cs +++ b/Nodify/Interactivity/InputElementState.cs @@ -1,4 +1,5 @@ -using System.Windows; +using System.Diagnostics; +using System.Windows; using System.Windows.Input; namespace Nodify.Interactivity diff --git a/Nodify/Nodes/Node.cs b/Nodify/Nodes/Node.cs index 7de14c0f..43e8f612 100644 --- a/Nodify/Nodes/Node.cs +++ b/Nodify/Nodes/Node.cs @@ -3,6 +3,7 @@ using System.Collections.Specialized; using System.Windows; using System.Windows.Controls; +using System.Windows.Input; using System.Windows.Media; namespace Nodify @@ -169,6 +170,10 @@ private static void OnFooterChanged(DependencyObject d, DependencyPropertyChange static Node() { DefaultStyleKeyProperty.OverrideMetadata(typeof(Node), new FrameworkPropertyMetadata(typeof(Node))); + FocusableProperty.OverrideMetadata(typeof(Node), new FrameworkPropertyMetadata(BoxValue.True)); + + KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(Node), new FrameworkPropertyMetadata(KeyboardNavigationMode.None)); + FocusManager.IsFocusScopeProperty.OverrideMetadata(typeof(Node), new FrameworkPropertyMetadata(BoxValue.True)); } public Node() diff --git a/Nodify/Themes/Styles/Node.xaml b/Nodify/Themes/Styles/Node.xaml index 624755cc..69eec810 100644 --- a/Nodify/Themes/Styles/Node.xaml +++ b/Nodify/Themes/Styles/Node.xaml @@ -99,7 +99,9 @@ ItemsSource="{TemplateBinding Input}" VerticalAlignment="{TemplateBinding VerticalAlignment}" ItemTemplate="{TemplateBinding InputConnectorTemplate}" - Focusable="False" /> + Focusable="False" + FocusManager.IsFocusScope="True" + KeyboardNavigation.DirectionalNavigation="Contained" /> + Focusable="False" + FocusManager.IsFocusScope="True" + KeyboardNavigation.DirectionalNavigation="Contained" /> diff --git a/Nodify/Utilities/EditorGesturesExtensions.cs b/Nodify/Utilities/EditorGesturesExtensions.cs new file mode 100644 index 00000000..28b12dad --- /dev/null +++ b/Nodify/Utilities/EditorGesturesExtensions.cs @@ -0,0 +1,67 @@ +using Nodify.Interactivity; +using System.Windows; +using System.Windows.Input; + +namespace Nodify +{ + internal static class EditorGesturesExtensions + { + public static SelectionType GetSelectionType(this EditorGestures.SelectionGestures gestures, InputEventArgs e) + { + if (gestures.Append.Matches(e.Source, e)) + { + return SelectionType.Append; + } + + if (gestures.Invert.Matches(e.Source, e)) + { + return SelectionType.Invert; + } + + if (gestures.Remove.Matches(e.Source, e)) + { + return SelectionType.Remove; + } + + return SelectionType.Replace; + } + + public static bool TryGetFocusDirection(this EditorGestures.DirectionalNavigationGestures gestures, InputEventArgs e, out FocusNavigationDirection direction) + { + direction = default; + + if (gestures.Left.Matches(e.Source, e)) + { + direction = FocusNavigationDirection.Left; + return true; + } + if (gestures.Right.Matches(e.Source, e)) + { + direction = FocusNavigationDirection.Right; + return true; + } + if (gestures.Up.Matches(e.Source, e)) + { + direction = FocusNavigationDirection.Up; + return true; + } + if (gestures.Down.Matches(e.Source, e)) + { + direction = FocusNavigationDirection.Down; + return true; + } + + return false; + } + + public static bool TryGetNavigationDirection(this EditorGestures.DirectionalNavigationGestures gestures, InputEventArgs e, out Vector direction) + { + double y = gestures.Up.Matches(e.Source, e) ? 1 : gestures.Down.Matches(e.Source, e) ? -1 : 0; + double x = gestures.Left.Matches(e.Source, e) ? -1 : gestures.Right.Matches(e.Source, e) ? 1 : 0; + + direction = new Vector(x, y); + + return x != 0 || y != 0; + } + } +} diff --git a/Nodify/Utilities/SelectionHelper.cs b/Nodify/Utilities/SelectionHelper.cs index 02a611de..cfc3baaf 100644 --- a/Nodify/Utilities/SelectionHelper.cs +++ b/Nodify/Utilities/SelectionHelper.cs @@ -192,27 +192,4 @@ private void ClearPreviewingSelection() #endregion } - - internal static class SelectionGesturesExtensions - { - public static SelectionType GetSelectionType(this EditorGestures.SelectionGestures gestures, InputEventArgs e) - { - if (gestures.Append.Matches(e.Source, e)) - { - return SelectionType.Append; - } - - if (gestures.Invert.Matches(e.Source, e)) - { - return SelectionType.Invert; - } - - if (gestures.Remove.Matches(e.Source, e)) - { - return SelectionType.Remove; - } - - return SelectionType.Replace; - } - } } diff --git a/Nodify/Utilities/WeakReferenceCollection.cs b/Nodify/Utilities/WeakReferenceCollection.cs new file mode 100644 index 00000000..40a7e876 --- /dev/null +++ b/Nodify/Utilities/WeakReferenceCollection.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Nodify +{ + /// + /// A collection of weak references to objects of type . + /// Automatically removes dead references after a configurable number of additions. + /// + /// The reference type stored in the collection. + internal class WeakReferenceCollection : IEnumerable where T : class + { + private readonly int _cleanupThreshold; + private readonly List> _references; + + private int _cleanupCounter = 0; + + /// + /// Initializes a new instance of the class. + /// + /// Initial capacity of the internal list. + /// Number of additions after which cleanup is triggered. + public WeakReferenceCollection(int initialCapacity, int cleanupThreshold = 32) + { + _cleanupThreshold = cleanupThreshold; + _references = new List>(initialCapacity); + } + + /// + /// Adds a new weak reference to the specified item to the collection. + /// Automatically triggers cleanup after a set number of additions. + /// + /// The item to add. + public void Add(T item) + { + _references.Add(new WeakReference(item)); + _cleanupCounter++; + if (_cleanupCounter >= _cleanupThreshold) + { + _cleanupCounter = 0; + Cleanup(); + } + } + + /// + /// Removes all weak references from the collection. + /// + public void Clear() + => _references.Clear(); + + /// + /// Returns a fast enumerator that iterates only over live references. + /// + public IEnumerator GetEnumerator() => new Enumerator(_references); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + /// + /// Removes dead references from the internal list. + /// + private void Cleanup() + { + int writeIndex = 0; + for (int readIndex = 0; readIndex < _references.Count; readIndex++) + { + var reference = _references[readIndex]; + if (reference.TryGetTarget(out _)) + { + if (writeIndex != readIndex) + _references[writeIndex] = reference; + writeIndex++; + } + } + + if (writeIndex < _references.Count) + { + _references.RemoveRange(writeIndex, _references.Count - writeIndex); + } + } + + /// + /// Struct-based enumerator for . + /// Efficiently enumerates live references only. + /// + public struct Enumerator : IEnumerator + { + private readonly List> _references; + private int _index; + private T? _current; + + /// + /// Initializes a new instance of the struct. + /// + /// The backing list of weak references. + public Enumerator(List> references) + { + _references = references; + _index = -1; + _current = default; + } + + public T Current => _current!; + object IEnumerator.Current => _current!; + + public bool MoveNext() + { + while (++_index < _references.Count) + { + if (_references[_index].TryGetTarget(out var target)) + { + _current = target; + return true; + } + } + + _current = null; + return false; + } + + public void Reset() + { + _index = -1; + _current = null; + } + + public readonly void Dispose() { } + } + } + +} diff --git a/README.md b/README.md index b68d9110..8c143c21 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,10 @@ Install-Package Nodify - Built-in dark and light **themes** - **Selecting**, **zooming**, **panning** with **auto panning** when close to edge - **Select**, **move** and **connect** nodes - - Lots of **configurable** dependency properties - Ready for undo/redo + - Configurable input gestures for each action + - Built-in keyboard navigation system + - Lots of **configurable** dependency properties - Example applications: 🎨 [**Playground**](Examples/Nodify.Playground), 🌓 [**State machine**](Examples/Nodify.StateMachine), 💻 [**Calculator**](Examples/Nodify.Calculator), 🔶 [**Canvas**](Examples/Nodify.Shapes) ## 📝 Documentation From 2f2e129a2a89ecf8b3e9856f819677218a35dc54 Mon Sep 17 00:00:00 2001 From: miroiu Date: Sun, 8 Jun 2025 00:06:10 +0300 Subject: [PATCH 02/28] Set default active layer --- Nodify/Editor/NodifyEditor.KeyboardNavigation.cs | 14 +++++++++----- Nodify/Editor/NodifyEditor.cs | 3 +-- Nodify/Editor/States/KeyboardNavigation.cs | 4 ++-- ...igationLayer.cs => IKeyboardNavigationLayer.cs} | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) rename Nodify/Interactivity/{INavigationLayer.cs => IKeyboardNavigationLayer.cs} (89%) diff --git a/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs b/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs index 516c5047..581392d4 100644 --- a/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs +++ b/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs @@ -8,7 +8,7 @@ namespace Nodify { - public partial class NodifyEditor : IKeyboardNavigationLayer, INavigationLayerGroup + public partial class NodifyEditor : IKeyboardNavigationLayer, IKeyboardNavigationLayerGroup { private readonly List _focusScopes = new List(); private IKeyboardNavigationLayer _activeFocusScope; @@ -141,17 +141,21 @@ public bool MoveFocus(FocusNavigationDirection direction) public new bool MoveFocus(TraversalRequest request) => ActiveLayer.TryMoveFocus(request); - void INavigationLayerGroup.RegisterLayer(IKeyboardNavigationLayer layer) + void IKeyboardNavigationLayerGroup.RegisterLayer(IKeyboardNavigationLayer layer) { _focusScopes.Add(layer); + if (ActiveLayer is null) + { + _activeFocusScope = layer; + } } - void INavigationLayerGroup.RemoveLayer(KeyboardNavigationLayerId layerId) + void IKeyboardNavigationLayerGroup.RemoveLayer(KeyboardNavigationLayerId layerId) { _focusScopes.Remove(_focusScopes.FirstOrDefault(layer => layer.Id == layerId)!); } - void INavigationLayerGroup.MoveToNextLayer() + void IKeyboardNavigationLayerGroup.MoveToNextLayer() { int currentIndex = _focusScopes.IndexOf(ActiveLayer); if (currentIndex >= 0 && currentIndex < _focusScopes.Count - 1) @@ -162,7 +166,7 @@ void INavigationLayerGroup.MoveToNextLayer() } } - void INavigationLayerGroup.MoveToPrevLayer() + void IKeyboardNavigationLayerGroup.MoveToPrevLayer() { throw new NotImplementedException(); } diff --git a/Nodify/Editor/NodifyEditor.cs b/Nodify/Editor/NodifyEditor.cs index 4a1d12df..5921fb24 100644 --- a/Nodify/Editor/NodifyEditor.cs +++ b/Nodify/Editor/NodifyEditor.cs @@ -581,8 +581,7 @@ public NodifyEditor() Unloaded += OnEditorUnloaded; - _focusScopes.Add(this); - _activeFocusScope = this; + ((IKeyboardNavigationLayerGroup)this).RegisterLayer(this); } /// diff --git a/Nodify/Editor/States/KeyboardNavigation.cs b/Nodify/Editor/States/KeyboardNavigation.cs index 21ab8fd7..bdda5145 100644 --- a/Nodify/Editor/States/KeyboardNavigation.cs +++ b/Nodify/Editor/States/KeyboardNavigation.cs @@ -62,12 +62,12 @@ protected override void OnKeyDown(KeyEventArgs e) } else if (gestures.NextNavigationLayer.Matches(e.Source, e)) { - ((INavigationLayerGroup)Element).MoveToNextLayer(); + ((IKeyboardNavigationLayerGroup)Element).MoveToNextLayer(); e.Handled = true; } else if (gestures.PrevNavigationLayer.Matches(e.Source, e)) { - ((INavigationLayerGroup)Element).MoveToPrevLayer(); + ((IKeyboardNavigationLayerGroup)Element).MoveToPrevLayer(); e.Handled = true; } } diff --git a/Nodify/Interactivity/INavigationLayer.cs b/Nodify/Interactivity/IKeyboardNavigationLayer.cs similarity index 89% rename from Nodify/Interactivity/INavigationLayer.cs rename to Nodify/Interactivity/IKeyboardNavigationLayer.cs index 5bc6c2cb..d21186eb 100644 --- a/Nodify/Interactivity/INavigationLayer.cs +++ b/Nodify/Interactivity/IKeyboardNavigationLayer.cs @@ -9,7 +9,7 @@ public class KeyboardNavigationLayerId public static readonly KeyboardNavigationLayerId Connections = new KeyboardNavigationLayerId(); } - public interface INavigationLayerGroup : IReadOnlyCollection + public interface IKeyboardNavigationLayerGroup : IReadOnlyCollection { IKeyboardNavigationLayer ActiveLayer { get; } From cd5c0d97cd8e054bfb78fe0935675979dab8f12e Mon Sep 17 00:00:00 2001 From: miroiu Date: Tue, 10 Jun 2025 19:14:00 +0300 Subject: [PATCH 03/28] Implemented more keyboard navigation gestures and layer management --- Examples/Nodify.Playground/App.xaml | 4 +- Nodify/Connections/ConnectionContainer.cs | 2 +- .../Connections/ConnectionsMultiSelector.cs | 36 ++++- .../Editor/NodifyEditor.KeyboardNavigation.cs | 147 ++++++++++++++---- Nodify/Editor/NodifyEditor.cs | 3 +- Nodify/Editor/States/KeyboardNavigation.cs | 72 ++++++--- .../Interactivity/Gestures/EditorGestures.cs | 21 ++- .../Interactivity/Gestures/KeyComboGesture.cs | 33 +++- .../Interactivity/IKeyboardNavigationLayer.cs | 16 +- Nodify/Minimap/MinimapItem.cs | 5 + Nodify/Utilities/EditorGesturesExtensions.cs | 9 ++ 11 files changed, 267 insertions(+), 81 deletions(-) diff --git a/Examples/Nodify.Playground/App.xaml b/Examples/Nodify.Playground/App.xaml index b0409fcf..00eb1241 100644 --- a/Examples/Nodify.Playground/App.xaml +++ b/Examples/Nodify.Playground/App.xaml @@ -14,7 +14,9 @@ - diff --git a/Nodify/Connections/ConnectionContainer.cs b/Nodify/Connections/ConnectionContainer.cs index 621fb760..4f912900 100644 --- a/Nodify/Connections/ConnectionContainer.cs +++ b/Nodify/Connections/ConnectionContainer.cs @@ -140,7 +140,7 @@ protected override void OnMouseUp(MouseButtonEventArgs e) /// Modifies the selection state of the current item based on the specified selection type. /// /// The type of selection to perform. - private void Select(SelectionType type) + public void Select(SelectionType type) { switch (type) { diff --git a/Nodify/Connections/ConnectionsMultiSelector.cs b/Nodify/Connections/ConnectionsMultiSelector.cs index 05969bfb..c241199c 100644 --- a/Nodify/Connections/ConnectionsMultiSelector.cs +++ b/Nodify/Connections/ConnectionsMultiSelector.cs @@ -1,13 +1,18 @@ -using System.Collections; +using Nodify.Interactivity; +using System.Collections; using System.Collections.Specialized; +using System.Diagnostics; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; +using System.Windows.Input; namespace Nodify { - internal sealed class ConnectionsMultiSelector : MultiSelector + internal sealed class ConnectionsMultiSelector : MultiSelector, IKeyboardNavigationLayer { + #region Dependency Properties + public static readonly DependencyProperty SelectedItemsProperty = NodifyEditor.SelectedItemsProperty.AddOwner(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(default(IList), OnSelectedItemsSourceChanged)); public static readonly DependencyProperty CanSelectMultipleItemsProperty = NodifyEditor.CanSelectMultipleItemsProperty.AddOwner(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(BoxValue.True, OnCanSelectMultipleItemsChanged, CoerceCanSelectMultipleItems)); @@ -44,11 +49,33 @@ private bool CanSelectMultipleItemsBase set => base.CanSelectMultipleItems = value; } + #endregion + /// /// Gets the that owns this . /// public NodifyEditor? Editor { get; private set; } + #region Keyboard Navigation + + KeyboardNavigationLayerId IKeyboardNavigationLayer.Id { get; } = KeyboardNavigationLayerId.Connections; + + bool IKeyboardNavigationLayer.TryMoveFocus(TraversalRequest request) + { + throw new System.NotImplementedException(); + } + + void IKeyboardNavigationLayer.OnActivate() + { + // TODO: Restore focus + } + + void IKeyboardNavigationLayer.OnDeactivate() + { + } + + #endregion + protected override DependencyObject GetContainerForItemOverride() => new ConnectionContainer(this); @@ -80,6 +107,11 @@ public override void OnApplyTemplate() base.OnApplyTemplate(); Editor = this.GetParentOfType(); + + if (Editor is IKeyboardNavigationLayerGroup group && group.RegisterLayer(this)) + { + Debug.WriteLine($"Registered {GetType().Name} as a keyboard navigation layer in {group.GetType().Name}"); + } } #region Selection Handlers diff --git a/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs b/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs index 581392d4..e5ff1971 100644 --- a/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs +++ b/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs @@ -5,31 +5,56 @@ using System.Windows.Input; using System.Windows; using System.Collections; +using System.Diagnostics; namespace Nodify { public partial class NodifyEditor : IKeyboardNavigationLayer, IKeyboardNavigationLayerGroup { - private readonly List _focusScopes = new List(); - private IKeyboardNavigationLayer _activeFocusScope; + private readonly List _navigationLayers = new List(); + private IKeyboardNavigationLayer? _activeKeyboardNavigationLayer; + private IKeyboardNavigationLayer KeyboardNavigationLayer => this; + private IKeyboardNavigationLayerGroup KeyboardNavigationLayerGroup => this; - public IKeyboardNavigationLayer ActiveLayer => _activeFocusScope; - - bool IKeyboardNavigationLayer.IsActiveLayer => ActiveLayer == this; + IKeyboardNavigationLayer? IKeyboardNavigationLayerGroup.ActiveLayer => _activeKeyboardNavigationLayer; KeyboardNavigationLayerId IKeyboardNavigationLayer.Id => KeyboardNavigationLayerId.Nodes; - int IReadOnlyCollection.Count => _focusScopes.Count; + int IReadOnlyCollection.Count => _navigationLayers.Count; // TODO: When do we clear these? + private readonly WeakReference _previousFocusedContainer = new WeakReference(null); + private FocusNavigationDirection? _previousFocusNavigationDirection; #region Focus Handling bool IKeyboardNavigationLayer.TryMoveFocus(TraversalRequest request) { - if (TryGetContainerToFocus(out var containerToFocus, request)) + // TODO: throw exception if request.FocusNavigationDirection is not directional (Left, Right, Up, Down) or handle other cases too + var prevContainer = Keyboard.FocusedElement as ItemContainer; + + if (_previousFocusNavigationDirection.HasValue && request.FocusNavigationDirection.IsOppositeOf(_previousFocusNavigationDirection.Value)) { - containerToFocus!.Focus(); + // If the request is in the opposite direction of the last focus navigation, try to restore the previous focused container + if (_previousFocusedContainer.TryGetTarget(out var previousContainer) && previousContainer.Focus()) + { + _previousFocusNavigationDirection = request.FocusNavigationDirection; + if (prevContainer != null) + { + _previousFocusedContainer.SetTarget(prevContainer); + } + BringIntoView(previousContainer, ItemContainer.BringIntoViewEdgeOffset); + return true; + } + + } + else if (TryGetContainerToFocus(out var containerToFocus, request) && containerToFocus!.Focus()) + { + _previousFocusNavigationDirection = request.FocusNavigationDirection; + if (prevContainer != null) + { + _previousFocusedContainer.SetTarget(prevContainer); + } BringIntoView(containerToFocus, ItemContainer.BringIntoViewEdgeOffset); return true; } @@ -43,13 +68,13 @@ private bool TryGetContainerToFocus(out ItemContainer? containerToFocus, Travers if (Keyboard.FocusedElement is ItemContainer focusedContainer) { - containerToFocus = GetDirectionalFocusTarget(focusedContainer, request); + containerToFocus = FindNextFocusTarget(focusedContainer, request); } else if (Keyboard.FocusedElement is NodifyEditor editor && editor.ItemContainers.Count > 0) { var viewport = new Rect(ViewportLocation, ViewportSize); containerToFocus = ItemContainers.FirstOrDefault(container => container.IsSelectableInArea(viewport, isContained: false)) - ?? ItemContainers.FirstOrDefault(); // TODO: Find the left most one? + ?? ItemContainers.First(); // TODO: Find the left most one? } else if (Keyboard.FocusedElement is UIElement elem && elem.GetParentOfType() is ItemContainer parentContainer) { @@ -59,8 +84,7 @@ private bool TryGetContainerToFocus(out ItemContainer? containerToFocus, Travers return containerToFocus != null; } - // TODO: Customizable - protected virtual ItemContainer? GetDirectionalFocusTarget(ItemContainer currentContainer, TraversalRequest request) + protected virtual ItemContainer? FindNextFocusTarget(ItemContainer currentContainer, TraversalRequest request) { var currentContainerBounds = new Rect(currentContainer.Location, currentContainer.DesiredSizeForSelection ?? currentContainer.RenderSize); var currentContainerCenter = new Point(currentContainerBounds.X + currentContainerBounds.Width / 2, currentContainerBounds.Y + currentContainerBounds.Height / 2); @@ -110,8 +134,8 @@ private bool TryGetContainerToFocus(out ItemContainer? containerToFocus, Travers FocusNavigationDirection.Right => itemContainers.OrderBy(c => c.Location.X).Take(1), FocusNavigationDirection.Up => itemContainers.OrderByDescending(c => c.Location.Y).Take(1), FocusNavigationDirection.Down => itemContainers.OrderBy(c => c.Location.Y).Take(1), - _ => Enumerable.Empty() - }; + _ => Enumerable.Empty() + }; request.Wrapped = true; } @@ -121,10 +145,19 @@ private bool TryGetContainerToFocus(out ItemContainer? containerToFocus, Travers foreach (var candidate in candidates) { - var bounds = new Rect(candidate.Location, candidate.DesiredSizeForSelection ?? candidate.RenderSize); - var center = new Point(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2); + // TODO: If candidate is on screen, give it priority over candidates that are off-screen - double distanceSquared = (center - currentContainerCenter).LengthSquared; + //var bounds = new Rect(candidate.Location, candidate.DesiredSizeForSelection ?? candidate.RenderSize); + //var center = new Point(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2); + + //double distanceSquared = (center - currentContainerCenter).LengthSquared; + //if (distanceSquared < minDistanceSquared) + //{ + // minDistanceSquared = distanceSquared; + // best = candidate; + //} + + double distanceSquared = (candidate.Location - currentContainerBounds.TopLeft).LengthSquared; if (distanceSquared < minDistanceSquared) { minDistanceSquared = distanceSquared; @@ -139,45 +172,91 @@ public bool MoveFocus(FocusNavigationDirection direction) => MoveFocus(new TraversalRequest(direction)); public new bool MoveFocus(TraversalRequest request) - => ActiveLayer.TryMoveFocus(request); + => KeyboardNavigationLayerGroup.ActiveLayer?.TryMoveFocus(request) ?? false; + + void IKeyboardNavigationLayer.OnActivate() + { + // TODO: Restore focus + } + + void IKeyboardNavigationLayer.OnDeactivate() + { + } + + #endregion + + #region Layer Management - void IKeyboardNavigationLayerGroup.RegisterLayer(IKeyboardNavigationLayer layer) + bool IKeyboardNavigationLayerGroup.RegisterLayer(IKeyboardNavigationLayer layer) { - _focusScopes.Add(layer); - if (ActiveLayer is null) + if (_navigationLayers.Any(l => l.Id == layer.Id)) { - _activeFocusScope = layer; + return false; } + + _navigationLayers.Add(layer); + if (KeyboardNavigationLayerGroup.ActiveLayer is null) + { + KeyboardNavigationLayerGroup.ActivateLayer(layer.Id); + } + + return true; } - void IKeyboardNavigationLayerGroup.RemoveLayer(KeyboardNavigationLayerId layerId) + bool IKeyboardNavigationLayerGroup.RemoveLayer(KeyboardNavigationLayerId layerId) { - _focusScopes.Remove(_focusScopes.FirstOrDefault(layer => layer.Id == layerId)!); + return _navigationLayers.Remove(_navigationLayers.FirstOrDefault(layer => layer.Id == layerId)!); } - void IKeyboardNavigationLayerGroup.MoveToNextLayer() + bool IKeyboardNavigationLayerGroup.ActivateLayer(KeyboardNavigationLayerId layerId) { - int currentIndex = _focusScopes.IndexOf(ActiveLayer); - if (currentIndex >= 0 && currentIndex < _focusScopes.Count - 1) + var layer = _navigationLayers.FirstOrDefault(x => x.Id == layerId); + if (layer != null) { - _activeFocusScope = _focusScopes[currentIndex + 1]; - // TODO: Focus container or logical element? - _activeFocusScope.TryMoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); + _activeKeyboardNavigationLayer?.OnDeactivate(); + _activeKeyboardNavigationLayer = layer; + _activeKeyboardNavigationLayer.OnActivate(); + Debug.WriteLine($"Activated {_activeKeyboardNavigationLayer.GetType().Name} as a keyboard navigation layer in {GetType().Name}"); + return true; } + + return false; } - void IKeyboardNavigationLayerGroup.MoveToPrevLayer() + bool IKeyboardNavigationLayerGroup.MoveToNextLayer() { - throw new NotImplementedException(); + Debug.Assert(KeyboardNavigationLayerGroup.ActiveLayer != null); + + int currentIndex = _navigationLayers.IndexOf(KeyboardNavigationLayerGroup.ActiveLayer!); + if (currentIndex >= 0 && currentIndex < _navigationLayers.Count - 1) + { + var layer = _navigationLayers[currentIndex + 1]; + return KeyboardNavigationLayerGroup.ActivateLayer(layer.Id); + } + + return false; + } + + bool IKeyboardNavigationLayerGroup.MoveToPrevLayer() + { + Debug.Assert(KeyboardNavigationLayerGroup.ActiveLayer != null); + + int currentIndex = _navigationLayers.IndexOf(KeyboardNavigationLayerGroup.ActiveLayer!); + if (currentIndex > 0) + { + var layer = _navigationLayers[currentIndex - 1]; + return KeyboardNavigationLayerGroup.ActivateLayer(layer.Id); + } + + return false; } public IEnumerator GetEnumerator() - => _focusScopes.GetEnumerator(); + => _navigationLayers.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); #endregion - } } diff --git a/Nodify/Editor/NodifyEditor.cs b/Nodify/Editor/NodifyEditor.cs index 5921fb24..05c97086 100644 --- a/Nodify/Editor/NodifyEditor.cs +++ b/Nodify/Editor/NodifyEditor.cs @@ -4,7 +4,6 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -581,7 +580,7 @@ public NodifyEditor() Unloaded += OnEditorUnloaded; - ((IKeyboardNavigationLayerGroup)this).RegisterLayer(this); + KeyboardNavigationLayerGroup.RegisterLayer(this); } /// diff --git a/Nodify/Editor/States/KeyboardNavigation.cs b/Nodify/Editor/States/KeyboardNavigation.cs index bdda5145..cf06b648 100644 --- a/Nodify/Editor/States/KeyboardNavigation.cs +++ b/Nodify/Editor/States/KeyboardNavigation.cs @@ -1,32 +1,17 @@ -using System; -using System.Runtime.CompilerServices; -using System.Windows; +using System.Windows; using System.Windows.Input; +using System.Xml.Linq; namespace Nodify.Interactivity { public static partial class EditorState { - // TODO: Move focus manually public class KeyboardNavigation : InputElementState { - public KeyboardNavigation(NodifyEditor element) : base(element) - { - ProcessHandledEvents = true; - } + private IKeyboardNavigationLayer? ActiveKeyboardNavigationLayer => ((IKeyboardNavigationLayerGroup)Element).ActiveLayer; - protected override void OnEvent(InputEventArgs e) - { - //if (e.RoutedEvent == UIElement.PreviewKeyDownEvent && e is KeyEventArgs args && IsDirectionalNavigationKey(args.Key)) - //{ - // OnKeyDown(args); - // e.Handled = true; - //} - } - - private static bool IsDirectionalNavigationKey(Key key) + public KeyboardNavigation(NodifyEditor element) : base(element) { - return key is Key.Left || key is Key.Right || key is Key.Up || key is Key.Down; } // TODO: If focus is within, do not allow escaping focus trap unless the escape gesture is performed. (some keys like Space or System Keys could try to escape) @@ -37,14 +22,13 @@ protected override void OnKeyDown(KeyEventArgs e) if (Element.IsKeyboardFocusWithin && IsEditorControl(e.OriginalSource)) { - // TODO: Check if the Editor.ActiveFocusScope (ActiveNavigationLayer) is the nodes layer if (gestures.Pan.TryGetNavigationDirection(e, out var panDirection)) { var panning = new Vector(-panDirection.X * cellSize, panDirection.Y * cellSize); Element.UpdatePanning(panning); e.Handled = true; } - else if (Element.SelectedContainersCount > 0 && gestures.MoveSelection.TryGetNavigationDirection(e, out var dragDirection)) + else if (CanDragSelection() && gestures.DragSelection.TryGetNavigationDirection(e, out var dragDirection)) { var dragging = new Vector(dragDirection.X * cellSize, -dragDirection.Y * cellSize); Element.BeginDragging(); @@ -60,17 +44,55 @@ protected override void OnKeyDown(KeyEventArgs e) Element.MoveFocus(direction); e.Handled = true; } - else if (gestures.NextNavigationLayer.Matches(e.Source, e)) + } + } + + protected override void OnKeyUp(KeyEventArgs e) + { + var gestures = EditorGestures.Mappings.Editor.Keyboard; + + if (gestures.ToggleSelected.Matches(e.Source, e)) + { + if (Keyboard.FocusedElement is ItemContainer itemContainer) + { + itemContainer.Select(SelectionType.Invert); + } + else if (Keyboard.FocusedElement is ConnectionContainer connectionContainer) + { + connectionContainer.Select(SelectionType.Invert); + } + + e.Handled = true; + } + else if (gestures.DeselectAll.Matches(e.Source, e)) + { + if (Element.SelectedContainersCount > 0 && ActiveKeyboardNavigationLayer?.Id == KeyboardNavigationLayerId.Nodes) { - ((IKeyboardNavigationLayerGroup)Element).MoveToNextLayer(); + Element.UnselectAll(); e.Handled = true; } - else if (gestures.PrevNavigationLayer.Matches(e.Source, e)) + // TODO: How to get the selected connections count without a hard reference to the connections multi selector? + else if (Element.SelectedConnections?.Count > 0 && ActiveKeyboardNavigationLayer?.Id == KeyboardNavigationLayerId.Connections) { - ((IKeyboardNavigationLayerGroup)Element).MoveToPrevLayer(); + Element.UnselectAllConnections(); e.Handled = true; } } + else if (gestures.NextNavigationLayer.Matches(e.Source, e)) + { + ((IKeyboardNavigationLayerGroup)Element).MoveToNextLayer(); + e.Handled = true; + } + else if (gestures.PrevNavigationLayer.Matches(e.Source, e)) + { + ((IKeyboardNavigationLayerGroup)Element).MoveToPrevLayer(); + e.Handled = true; + } + } + + private bool CanDragSelection() + { + return ActiveKeyboardNavigationLayer?.Id == KeyboardNavigationLayerId.Nodes && Element.SelectedContainersCount > 0; } // TODO: Allow for extensibility because connections can be custom diff --git a/Nodify/Interactivity/Gestures/EditorGestures.cs b/Nodify/Interactivity/Gestures/EditorGestures.cs index a68b5a4d..a88d5c7b 100644 --- a/Nodify/Interactivity/Gestures/EditorGestures.cs +++ b/Nodify/Interactivity/Gestures/EditorGestures.cs @@ -121,7 +121,7 @@ public ItemContainerGestures() } /// Gesture to select the container using a strategy. - /// Defaults to or any of the gestures. + /// Defaults to or any of the gestures. public SelectionGestures Selection { get; } /// Gesture to start and complete a dragging operation. @@ -204,25 +204,32 @@ public class KeyboardNavigation public KeyboardNavigation() { Pan = new DirectionalNavigationGestures(Key.Space, repeated: true); - MoveSelection = new DirectionalNavigationGestures(ModifierKeys.Control); + 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); } // TODO: Pan large step gesture? public DirectionalNavigationGestures Pan { get; } - - public DirectionalNavigationGestures MoveSelection { get; } - + public DirectionalNavigationGestures DragSelection { get; } public DirectionalNavigationGestures NavigateSelection { get; } + public InputGestureRef ToggleSelected { get; } + public InputGestureRef DeselectAll { get; } + public InputGestureRef NextNavigationLayer { get; } public InputGestureRef PrevNavigationLayer { get; } public void Apply(KeyboardNavigation 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; } @@ -230,6 +237,10 @@ public void Apply(KeyboardNavigation gestures) public void Unbind() { Pan.Unbind(); + DragSelection.Unbind(); + NavigateSelection.Unbind(); + ToggleSelected.Unbind(); + DeselectAll.Unbind(); NextNavigationLayer.Unbind(); PrevNavigationLayer.Unbind(); } diff --git a/Nodify/Interactivity/Gestures/KeyComboGesture.cs b/Nodify/Interactivity/Gestures/KeyComboGesture.cs index 6dffb628..f9964efe 100644 --- a/Nodify/Interactivity/Gestures/KeyComboGesture.cs +++ b/Nodify/Interactivity/Gestures/KeyComboGesture.cs @@ -12,6 +12,12 @@ internal class KeyComboGesture : KeyGesture private static readonly WeakReferenceCollection _allCombos = new WeakReferenceCollection(16); private bool _isTriggerDown; + private int _comboCounter; + + /// + /// Gets a value indicating whether the combo gesture has been performed at least once. + /// + private bool HasBeenPerformedAtLeastOnce => _comboCounter > 0; /// /// Gets or sets the key that must be pressed first to activate this combo gesture. @@ -27,7 +33,6 @@ internal class KeyComboGesture : KeyGesture static KeyComboGesture() { EventManager.RegisterClassHandler(typeof(UIElement), UIElement.PreviewKeyUpEvent, new KeyEventHandler(HandleKeyUp), true); - EventManager.RegisterClassHandler(typeof(UIElement), UIElement.LostKeyboardFocusEvent, new KeyboardFocusChangedEventHandler(HandleFocusLost), true); } @@ -68,7 +73,7 @@ private static void HandleFocusLost(object sender, KeyboardFocusChangedEventArgs { foreach (var combo in _allCombos) { - combo._isTriggerDown = false; + combo.Reset(); } } @@ -76,13 +81,24 @@ private static void HandleKeyUp(object sender, KeyEventArgs e) { foreach (var combo in _allCombos) { - if (combo._isTriggerDown && e.Key == combo.TriggerKey) + if (e.Key == combo.TriggerKey) { - combo._isTriggerDown = false; + // 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) @@ -94,7 +110,14 @@ public override bool Matches(object targetElement, InputEventArgs inputEventArgs // The combo key only triggers the combo on key down bool matches = _isTriggerDown && base.Matches(targetElement, inputEventArgs); - if (matches && !AllowRepeatingComboKey) + if (!matches) + { + return false; + } + + _comboCounter++; + + if (!AllowRepeatingComboKey) { _isTriggerDown = false; } diff --git a/Nodify/Interactivity/IKeyboardNavigationLayer.cs b/Nodify/Interactivity/IKeyboardNavigationLayer.cs index d21186eb..14367270 100644 --- a/Nodify/Interactivity/IKeyboardNavigationLayer.cs +++ b/Nodify/Interactivity/IKeyboardNavigationLayer.cs @@ -7,19 +7,22 @@ public class KeyboardNavigationLayerId { public static readonly KeyboardNavigationLayerId Nodes = new KeyboardNavigationLayerId(); public static readonly KeyboardNavigationLayerId Connections = new KeyboardNavigationLayerId(); + public static readonly KeyboardNavigationLayerId Decorators = new KeyboardNavigationLayerId(); } public interface IKeyboardNavigationLayerGroup : IReadOnlyCollection { - IKeyboardNavigationLayer ActiveLayer { get; } + IKeyboardNavigationLayer? ActiveLayer { get; } - void MoveToNextLayer(); + bool MoveToNextLayer(); - void MoveToPrevLayer(); + bool MoveToPrevLayer(); - void RegisterLayer(IKeyboardNavigationLayer layer); + bool RegisterLayer(IKeyboardNavigationLayer layer); - void RemoveLayer(KeyboardNavigationLayerId layerId); + bool RemoveLayer(KeyboardNavigationLayerId layerId); + + bool ActivateLayer(KeyboardNavigationLayerId layerId); } public interface IKeyboardNavigationLayer @@ -28,6 +31,7 @@ public interface IKeyboardNavigationLayer bool TryMoveFocus(TraversalRequest request); - bool IsActiveLayer { get; } + void OnActivate(); + void OnDeactivate(); } } diff --git a/Nodify/Minimap/MinimapItem.cs b/Nodify/Minimap/MinimapItem.cs index d792a83b..f60de8c9 100644 --- a/Nodify/Minimap/MinimapItem.cs +++ b/Nodify/Minimap/MinimapItem.cs @@ -5,6 +5,11 @@ namespace Nodify { public class MinimapItem : ContentControl { + static MinimapItem() + { + FocusableProperty.OverrideMetadata(typeof(MinimapItem), new FrameworkPropertyMetadata(BoxValue.False)); + } + public static readonly DependencyProperty LocationProperty = ItemContainer.LocationProperty.AddOwner(typeof(MinimapItem), new FrameworkPropertyMetadata(BoxValue.Point, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.AffectsParentMeasure)); /// diff --git a/Nodify/Utilities/EditorGesturesExtensions.cs b/Nodify/Utilities/EditorGesturesExtensions.cs index 28b12dad..d6e63d47 100644 --- a/Nodify/Utilities/EditorGesturesExtensions.cs +++ b/Nodify/Utilities/EditorGesturesExtensions.cs @@ -63,5 +63,14 @@ public static bool TryGetNavigationDirection(this EditorGestures.DirectionalNavi return x != 0 || y != 0; } + + // TODO: Handle all cases? + public static bool IsOppositeOf(this FocusNavigationDirection direction, FocusNavigationDirection other) + { + return (direction == FocusNavigationDirection.Left && other == FocusNavigationDirection.Right) || + (direction == FocusNavigationDirection.Right && other == FocusNavigationDirection.Left) || + (direction == FocusNavigationDirection.Up && other == FocusNavigationDirection.Down) || + (direction == FocusNavigationDirection.Down && other == FocusNavigationDirection.Up); + } } } From b204f129fd0fc10216a7c9a2bc8b06f6d924b23a Mon Sep 17 00:00:00 2001 From: miroiu Date: Tue, 10 Jun 2025 21:45:05 +0300 Subject: [PATCH 04/28] Connections navigation --- Nodify/Connections/BaseConnection.cs | 5 +- Nodify/Connections/ConnectionContainer.cs | 20 ++- .../Connections/ConnectionsMultiSelector.cs | 98 ++++++++++++- Nodify/Containers/ItemContainer.cs | 7 +- .../Editor/NodifyEditor.KeyboardNavigation.cs | 85 +---------- Nodify/Editor/NodifyEditor.cs | 2 +- Nodify/Editor/States/KeyboardNavigation.cs | 3 +- .../Interactivity/Gestures/EditorGestures.cs | 8 +- Nodify/Interactivity/IKeyboardFocusTarget.cs | 134 ++++++++++++++++++ .../Interactivity/IKeyboardNavigationLayer.cs | 2 + 10 files changed, 266 insertions(+), 98 deletions(-) create mode 100644 Nodify/Interactivity/IKeyboardFocusTarget.cs diff --git a/Nodify/Connections/BaseConnection.cs b/Nodify/Connections/BaseConnection.cs index 792318ee..a40f8798 100644 --- a/Nodify/Connections/BaseConnection.cs +++ b/Nodify/Connections/BaseConnection.cs @@ -109,7 +109,7 @@ public enum ArrowHeadShape /// /// Represents the base class for shapes that are drawn from a point to a point. /// - public abstract class BaseConnection : Shape + public abstract class BaseConnection : Shape, IKeyboardFocusTarget { #region Dependency Properties @@ -491,6 +491,9 @@ public event ConnectionEventHandler Split /// protected static readonly Vector ZeroVector = new Vector(0d, 0d); + Rect IKeyboardFocusTarget.Bounds => new Rect(Source, Target); + FrameworkElement IKeyboardFocusTarget.Element => this; + private Pen? _outlinePen; private readonly StreamGeometry _geometry = new StreamGeometry diff --git a/Nodify/Connections/ConnectionContainer.cs b/Nodify/Connections/ConnectionContainer.cs index 4f912900..315ad5e0 100644 --- a/Nodify/Connections/ConnectionContainer.cs +++ b/Nodify/Connections/ConnectionContainer.cs @@ -1,11 +1,12 @@ using Nodify.Interactivity; +using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace Nodify { - internal sealed class ConnectionContainer : ContentPresenter + public class ConnectionContainer : ContentPresenter, IKeyboardFocusTarget { #region Dependency properties @@ -66,6 +67,13 @@ public event RoutedEventHandler Unselected #endregion + Rect IKeyboardFocusTarget.Bounds => ConnectionFocusTarget.Bounds; + ConnectionContainer IKeyboardFocusTarget.Element => this; + + // TODO: + private IKeyboardFocusTarget ConnectionFocusTarget => Connection as IKeyboardFocusTarget + ?? throw new NotSupportedException($"Custom connections must implement {nameof(IKeyboardFocusTarget)} for keyboard navigation. Or disable keyboard navigation for the connections layer."); + private ConnectionsMultiSelector Selector { get; } private FrameworkElement? _connection; @@ -78,6 +86,9 @@ public event RoutedEventHandler Unselected static ConnectionContainer() { FocusableProperty.OverrideMetadata(typeof(ConnectionContainer), new FrameworkPropertyMetadata(BoxValue.True)); + + // TODO: + FocusVisualStyleProperty.OverrideMetadata(typeof(ConnectionContainer), new FrameworkPropertyMetadata(null)); } internal ConnectionContainer(ConnectionsMultiSelector selector) @@ -85,6 +96,13 @@ internal ConnectionContainer(ConnectionsMultiSelector selector) Selector = selector; } + protected override void OnIsKeyboardFocusedChanged(DependencyPropertyChangedEventArgs e) + { + // TODO: Custom property on BaseConnection + BaseConnection.SetIsSelected(Connection, (bool)e.NewValue); + //Connection?.SetValue(IsKeyboardFocusedProperty, e.NewValue); + } + /// /// Raises the or based on . /// Called when the value is changed. diff --git a/Nodify/Connections/ConnectionsMultiSelector.cs b/Nodify/Connections/ConnectionsMultiSelector.cs index c241199c..d7215956 100644 --- a/Nodify/Connections/ConnectionsMultiSelector.cs +++ b/Nodify/Connections/ConnectionsMultiSelector.cs @@ -1,5 +1,7 @@ using Nodify.Interactivity; +using System; using System.Collections; +using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Windows; @@ -9,7 +11,7 @@ namespace Nodify { - internal sealed class ConnectionsMultiSelector : MultiSelector, IKeyboardNavigationLayer + public class ConnectionsMultiSelector : MultiSelector, IKeyboardNavigationLayer { #region Dependency Properties @@ -56,18 +58,108 @@ private bool CanSelectMultipleItemsBase /// public NodifyEditor? Editor { get; private set; } + /// + /// Gets a list of all s. + /// + /// Cache the result before using it to avoid extra allocations. + protected internal IReadOnlyCollection ConnectionContainers + { + get + { + ItemCollection items = Items; + var containers = new List(items.Count); + + for (var i = 0; i < items.Count; i++) + { + containers.Add((ConnectionContainer)ItemContainerGenerator.ContainerFromIndex(i)); + } + + return containers; + } + } + + static ConnectionsMultiSelector() + { + FocusableProperty.OverrideMetadata(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(BoxValue.False)); + + KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(KeyboardNavigationMode.Once)); + KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(KeyboardNavigationMode.None)); + FocusManager.IsFocusScopeProperty.OverrideMetadata(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(BoxValue.True)); + } + #region Keyboard Navigation KeyboardNavigationLayerId IKeyboardNavigationLayer.Id { get; } = KeyboardNavigationLayerId.Connections; + private readonly WeakReference _previousFocusedContainer = new WeakReference(null!); + private FocusNavigationDirection? _previousFocusNavigationDirection; + bool IKeyboardNavigationLayer.TryMoveFocus(TraversalRequest request) { - throw new System.NotImplementedException(); + // TODO: throw exception if request.FocusNavigationDirection is not directional (Left, Right, Up, Down) or handle other cases too + var prevContainer = Keyboard.FocusedElement as ConnectionContainer; + + if (_previousFocusNavigationDirection.HasValue && request.FocusNavigationDirection.IsOppositeOf(_previousFocusNavigationDirection.Value)) + { + // If the request is in the opposite direction of the last focus navigation, try to restore the previous focused container + if (_previousFocusedContainer.TryGetTarget(out var previousContainer) && previousContainer.Focus()) + { + _previousFocusNavigationDirection = request.FocusNavigationDirection; + if (prevContainer != null) + { + _previousFocusedContainer.SetTarget(prevContainer); + } + + // TODO: Bring into view? + return true; + } + } + else if (TryGetContainerToFocus(out var containerToFocus, request) && containerToFocus!.Focus()) + { + _previousFocusNavigationDirection = request.FocusNavigationDirection; + if (prevContainer != null) + { + _previousFocusedContainer.SetTarget(prevContainer); + } + + // TODO: Bring into view? + return true; + } + + return false; + } + + private bool TryGetContainerToFocus(out ConnectionContainer? containerToFocus, TraversalRequest request) + { + containerToFocus = null; + + if (Keyboard.FocusedElement is ConnectionContainer focusedContainer) + { + containerToFocus = FindNextFocusTarget(focusedContainer, request); + } + else if (Keyboard.FocusedElement is UIElement elem && elem.GetParentOfType() is ConnectionContainer parentContainer) + { + containerToFocus = parentContainer; + } + + return containerToFocus != null; + } + + protected virtual ConnectionContainer? FindNextFocusTarget(ConnectionContainer currentContainer, TraversalRequest request) + { + var focusNavigator = new LinearFocusNavigator(ConnectionContainers); + var result = focusNavigator.FindNextFocusTarget(currentContainer, request); + + return result?.Element; } void IKeyboardNavigationLayer.OnActivate() { - // TODO: Restore focus + if (Items.Count > 0) + { + var container = (ConnectionContainer)ItemContainerGenerator.ContainerFromIndex(0); + container.Focus(); + } } void IKeyboardNavigationLayer.OnDeactivate() diff --git a/Nodify/Containers/ItemContainer.cs b/Nodify/Containers/ItemContainer.cs index aa3d76c5..eb62238f 100644 --- a/Nodify/Containers/ItemContainer.cs +++ b/Nodify/Containers/ItemContainer.cs @@ -11,7 +11,7 @@ namespace Nodify /// /// The container for all the items generated by the of the . /// - public class ItemContainer : ContentControl, INodifyCanvasItem + public class ItemContainer : ContentControl, INodifyCanvasItem, IKeyboardFocusTarget { #region Dependency Properties @@ -257,6 +257,8 @@ private static void OnIsSelectedChanged(DependencyObject d, DependencyPropertyCh /// public Rect Bounds => new Rect(Location, DesiredSizeForSelection ?? RenderSize); + ItemContainer IKeyboardFocusTarget.Element => this; + #endregion /// @@ -322,8 +324,7 @@ protected internal virtual bool IsSelectableLocation(Point position) /// True if contains or intersects this . public virtual bool IsSelectableInArea(Rect area, bool isContained) { - var bounds = Bounds; - return isContained ? area.Contains(bounds) : area.IntersectsWith(bounds); + return isContained ? area.Contains(Bounds) : area.IntersectsWith(Bounds); } /// diff --git a/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs b/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs index e5ff1971..bf125887 100644 --- a/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs +++ b/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs @@ -43,10 +43,10 @@ bool IKeyboardNavigationLayer.TryMoveFocus(TraversalRequest request) { _previousFocusedContainer.SetTarget(prevContainer); } + BringIntoView(previousContainer, ItemContainer.BringIntoViewEdgeOffset); return true; } - } else if (TryGetContainerToFocus(out var containerToFocus, request) && containerToFocus!.Focus()) { @@ -55,6 +55,7 @@ bool IKeyboardNavigationLayer.TryMoveFocus(TraversalRequest request) { _previousFocusedContainer.SetTarget(prevContainer); } + BringIntoView(containerToFocus, ItemContainer.BringIntoViewEdgeOffset); return true; } @@ -86,86 +87,10 @@ private bool TryGetContainerToFocus(out ItemContainer? containerToFocus, Travers protected virtual ItemContainer? FindNextFocusTarget(ItemContainer currentContainer, TraversalRequest request) { - var currentContainerBounds = new Rect(currentContainer.Location, currentContainer.DesiredSizeForSelection ?? currentContainer.RenderSize); - var currentContainerCenter = new Point(currentContainerBounds.X + currentContainerBounds.Width / 2, currentContainerBounds.Y + currentContainerBounds.Height / 2); - - //IEnumerable candidates = request.FocusNavigationDirection switch - //{ - // FocusNavigationDirection.Left => ItemContainers.Where(c => - // { - // var center = new Point(c.Bounds.X + c.Bounds.Width / 2, c.Bounds.Y + c.Bounds.Height / 2); - // return center.X < currentContainerCenter.X; - // }), - // FocusNavigationDirection.Right => ItemContainers.Where(c => - // { - // var center = new Point(c.Bounds.X + c.Bounds.Width / 2, c.Bounds.Y + c.Bounds.Height / 2); - // return center.X > currentContainerCenter.X; - // }), - // FocusNavigationDirection.Up => ItemContainers.Where(c => - // { - // var center = new Point(c.Bounds.X + c.Bounds.Width / 2, c.Bounds.Y + c.Bounds.Height / 2); - // return center.Y < currentContainerCenter.Y; - // }), - // FocusNavigationDirection.Down => ItemContainers.Where(c => - // { - // var center = new Point(c.Bounds.X + c.Bounds.Width / 2, c.Bounds.Y + c.Bounds.Height / 2); - // return center.Y > currentContainerCenter.Y; - // }), - // _ => Enumerable.Empty() - //}; - - var itemContainers = ItemContainers; - - IEnumerable candidates = request.FocusNavigationDirection switch - { - FocusNavigationDirection.Left => itemContainers.Where(c => c.Bounds.Right <= currentContainerBounds.Left), - FocusNavigationDirection.Right => itemContainers.Where(c => c.Location.X >= currentContainerBounds.Right), - FocusNavigationDirection.Up => itemContainers.Where(c => c.Bounds.Bottom <= currentContainerBounds.Top), - FocusNavigationDirection.Down => itemContainers.Where(c => c.Location.Y >= currentContainerBounds.Bottom), - _ => Enumerable.Empty() - }; - - // Wrap focus if no candidates found in the current direction - if (!candidates.Any()) - { - candidates = request.FocusNavigationDirection switch - { - FocusNavigationDirection.Left => itemContainers.OrderByDescending(c => c.Location.X).Take(1), - FocusNavigationDirection.Right => itemContainers.OrderBy(c => c.Location.X).Take(1), - FocusNavigationDirection.Up => itemContainers.OrderByDescending(c => c.Location.Y).Take(1), - FocusNavigationDirection.Down => itemContainers.OrderBy(c => c.Location.Y).Take(1), - _ => Enumerable.Empty() - }; - - request.Wrapped = true; - } - - ItemContainer? best = null; - double minDistanceSquared = double.MaxValue; - - foreach (var candidate in candidates) - { - // TODO: If candidate is on screen, give it priority over candidates that are off-screen - - //var bounds = new Rect(candidate.Location, candidate.DesiredSizeForSelection ?? candidate.RenderSize); - //var center = new Point(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2); - - //double distanceSquared = (center - currentContainerCenter).LengthSquared; - //if (distanceSquared < minDistanceSquared) - //{ - // minDistanceSquared = distanceSquared; - // best = candidate; - //} - - double distanceSquared = (candidate.Location - currentContainerBounds.TopLeft).LengthSquared; - if (distanceSquared < minDistanceSquared) - { - minDistanceSquared = distanceSquared; - best = candidate; - } - } + var focusNavigator = new DirectionalFocusNavigator(ItemContainers); + var result = focusNavigator.FindNextFocusTarget(currentContainer, request); - return best; + return result?.Element; } public bool MoveFocus(FocusNavigationDirection direction) diff --git a/Nodify/Editor/NodifyEditor.cs b/Nodify/Editor/NodifyEditor.cs index 05c97086..d8d81f25 100644 --- a/Nodify/Editor/NodifyEditor.cs +++ b/Nodify/Editor/NodifyEditor.cs @@ -552,7 +552,7 @@ static NodifyEditor() DefaultStyleKeyProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(typeof(NodifyEditor))); FocusableProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.True)); - KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(KeyboardNavigationMode.None)); + KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(KeyboardNavigationMode.Once)); KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(KeyboardNavigationMode.None)); FocusManager.IsFocusScopeProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.True)); diff --git a/Nodify/Editor/States/KeyboardNavigation.cs b/Nodify/Editor/States/KeyboardNavigation.cs index cf06b648..2928f54f 100644 --- a/Nodify/Editor/States/KeyboardNavigation.cs +++ b/Nodify/Editor/States/KeyboardNavigation.cs @@ -1,6 +1,5 @@ using System.Windows; using System.Windows.Input; -using System.Xml.Linq; namespace Nodify.Interactivity { @@ -98,7 +97,7 @@ private bool CanDragSelection() // TODO: Allow for extensibility because connections can be custom private static bool IsEditorControl(object originalSource) { - return originalSource is NodifyEditor || originalSource is ItemContainer || originalSource is Connector || originalSource is BaseConnection; + return originalSource is NodifyEditor || originalSource is ItemContainer || originalSource is Connector || originalSource is ConnectionContainer; } } } diff --git a/Nodify/Interactivity/Gestures/EditorGestures.cs b/Nodify/Interactivity/Gestures/EditorGestures.cs index a88d5c7b..5cbd790e 100644 --- a/Nodify/Interactivity/Gestures/EditorGestures.cs +++ b/Nodify/Interactivity/Gestures/EditorGestures.cs @@ -246,15 +246,9 @@ public void Unbind() } } // TODO: - // Pan editor: Space+Arrow keys - // // Navigate connections = Arrow keys - // Navigate nodes = Arrow keys // Navigate connectors inside panel = Arrow keys - // Toggle selection = CTRL+Space or Enter - // Move nodes: - CTRL + Arrow Keys – nudge selected node(s) by 1 unit - // - Shift + CTRL + Arrow Keys – nudge by 10 units - // Deselect all = Escape + // Move nodes: - Shift + CTRL + Arrow Keys – nudge by 10 units public NodifyEditorGestures() { diff --git a/Nodify/Interactivity/IKeyboardFocusTarget.cs b/Nodify/Interactivity/IKeyboardFocusTarget.cs new file mode 100644 index 00000000..b4084064 --- /dev/null +++ b/Nodify/Interactivity/IKeyboardFocusTarget.cs @@ -0,0 +1,134 @@ +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Input; + +namespace Nodify.Interactivity +{ + public interface IKeyboardFocusTarget + where TElement : UIElement + { + Rect Bounds { get; } + TElement Element { get; } + } + + internal readonly struct DirectionalFocusNavigator + where TElement : UIElement, IKeyboardFocusTarget + { + private readonly IEnumerable> _availableTargets; + + public DirectionalFocusNavigator(IEnumerable> availableTargets) + { + _availableTargets = availableTargets; + } + + public readonly IKeyboardFocusTarget? FindNextFocusTarget(IKeyboardFocusTarget currentContainer, TraversalRequest request) + { + var currentContainerBounds = currentContainer.Bounds; + + IEnumerable> candidates = request.FocusNavigationDirection switch + { + FocusNavigationDirection.Left => _availableTargets.Where(c => c.Bounds.Right <= currentContainerBounds.Left), + FocusNavigationDirection.Right => _availableTargets.Where(c => c.Bounds.Left >= currentContainerBounds.Right), + FocusNavigationDirection.Up => _availableTargets.Where(c => c.Bounds.Bottom <= currentContainerBounds.Top), + FocusNavigationDirection.Down => _availableTargets.Where(c => c.Bounds.Top >= currentContainerBounds.Bottom), + _ => Enumerable.Empty>() + }; + + // 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), + _ => Enumerable.Empty>() + }; + + request.Wrapped = true; + } + + IKeyboardFocusTarget? 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; + } + } + + internal readonly struct LinearFocusNavigator + where TElement : UIElement, IKeyboardFocusTarget + { + private enum LinearNavigationDirection + { + First, + Last, + Forward, + Backward + } + + private readonly IEnumerable> _availableTargets; + + public LinearFocusNavigator(IEnumerable> availableTargets) + { + _availableTargets = availableTargets; + } + + public readonly IKeyboardFocusTarget? FindNextFocusTarget(IKeyboardFocusTarget currentContainer, TraversalRequest request) + { + var currentContainerBounds = currentContainer.Bounds; + + 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> ?? _availableTargets.ToList(); + var currentIndex = availableTargets.IndexOf(currentContainer); + + IKeyboardFocusTarget? 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; + } + } +} diff --git a/Nodify/Interactivity/IKeyboardNavigationLayer.cs b/Nodify/Interactivity/IKeyboardNavigationLayer.cs index 14367270..8f8c0776 100644 --- a/Nodify/Interactivity/IKeyboardNavigationLayer.cs +++ b/Nodify/Interactivity/IKeyboardNavigationLayer.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; +using System.Windows; using System.Windows.Input; namespace Nodify.Interactivity { + public class KeyboardNavigationLayerId { public static readonly KeyboardNavigationLayerId Nodes = new KeyboardNavigationLayerId(); From 698442851be6f730fa7ab03f1db660f89d77740a Mon Sep 17 00:00:00 2001 From: miroiu Date: Wed, 11 Jun 2025 20:56:47 +0300 Subject: [PATCH 05/28] Add custom connection focus navigator and remove focus style on connection container --- Nodify/Connections/BaseConnection.cs | 3 +- Nodify/Connections/ConnectionContainer.cs | 7 +- .../Connections/ConnectionsMultiSelector.cs | 40 +--------- .../Editor/NodifyEditor.KeyboardNavigation.cs | 40 +--------- .../ConnectionFocusNavigator.cs | 47 ++++++++++++ .../DirectionalFocusNavigator.cs | 73 +++++++++++++++++++ .../IKeyboardNavigationLayer.cs | 8 +- .../LinearFocusNavigator.cs} | 66 +---------------- .../StatefulFocusNavigator.cs | 44 +++++++++++ Nodify/Utilities/EditorGesturesExtensions.cs | 13 ++-- 10 files changed, 193 insertions(+), 148 deletions(-) create mode 100644 Nodify/Interactivity/KeyboardNavigation/ConnectionFocusNavigator.cs create mode 100644 Nodify/Interactivity/KeyboardNavigation/DirectionalFocusNavigator.cs rename Nodify/Interactivity/{ => KeyboardNavigation}/IKeyboardNavigationLayer.cs (87%) rename Nodify/Interactivity/{IKeyboardFocusTarget.cs => KeyboardNavigation/LinearFocusNavigator.cs} (50%) create mode 100644 Nodify/Interactivity/KeyboardNavigation/StatefulFocusNavigator.cs diff --git a/Nodify/Connections/BaseConnection.cs b/Nodify/Connections/BaseConnection.cs index a40f8798..d053a2a8 100644 --- a/Nodify/Connections/BaseConnection.cs +++ b/Nodify/Connections/BaseConnection.cs @@ -491,7 +491,8 @@ public event ConnectionEventHandler Split /// protected static readonly Vector ZeroVector = new Vector(0d, 0d); - Rect IKeyboardFocusTarget.Bounds => new Rect(Source, Target); + // Use Source for both corners to ensure Top-Left aligns with intended keyboard focus point. + Rect IKeyboardFocusTarget.Bounds => new Rect(Source, Source); FrameworkElement IKeyboardFocusTarget.Element => this; private Pen? _outlinePen; diff --git a/Nodify/Connections/ConnectionContainer.cs b/Nodify/Connections/ConnectionContainer.cs index 315ad5e0..d88e6c02 100644 --- a/Nodify/Connections/ConnectionContainer.cs +++ b/Nodify/Connections/ConnectionContainer.cs @@ -74,10 +74,10 @@ public event RoutedEventHandler Unselected private IKeyboardFocusTarget ConnectionFocusTarget => Connection as IKeyboardFocusTarget ?? throw new NotSupportedException($"Custom connections must implement {nameof(IKeyboardFocusTarget)} for keyboard navigation. Or disable keyboard navigation for the connections layer."); - private ConnectionsMultiSelector Selector { get; } + public ConnectionsMultiSelector Selector { get; } private FrameworkElement? _connection; - private FrameworkElement? Connection => _connection ??= BaseConnection.PrioritizeBaseConnectionForSelection + public FrameworkElement? Connection => _connection ??= BaseConnection.PrioritizeBaseConnectionForSelection ? this.GetChildOfType() ?? this.GetChildOfType() : this.GetChildOfType(); @@ -87,8 +87,7 @@ static ConnectionContainer() { FocusableProperty.OverrideMetadata(typeof(ConnectionContainer), new FrameworkPropertyMetadata(BoxValue.True)); - // TODO: - FocusVisualStyleProperty.OverrideMetadata(typeof(ConnectionContainer), new FrameworkPropertyMetadata(null)); + FocusVisualStyleProperty.OverrideMetadata(typeof(ConnectionContainer), new FrameworkPropertyMetadata(new Style())); } internal ConnectionContainer(ConnectionsMultiSelector selector) diff --git a/Nodify/Connections/ConnectionsMultiSelector.cs b/Nodify/Connections/ConnectionsMultiSelector.cs index d7215956..18a06bae 100644 --- a/Nodify/Connections/ConnectionsMultiSelector.cs +++ b/Nodify/Connections/ConnectionsMultiSelector.cs @@ -1,5 +1,4 @@ using Nodify.Interactivity; -using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; @@ -91,45 +90,14 @@ static ConnectionsMultiSelector() KeyboardNavigationLayerId IKeyboardNavigationLayer.Id { get; } = KeyboardNavigationLayerId.Connections; - private readonly WeakReference _previousFocusedContainer = new WeakReference(null!); - private FocusNavigationDirection? _previousFocusNavigationDirection; + private readonly StatefulFocusNavigator _focusNavigator = new StatefulFocusNavigator(); bool IKeyboardNavigationLayer.TryMoveFocus(TraversalRequest request) { - // TODO: throw exception if request.FocusNavigationDirection is not directional (Left, Right, Up, Down) or handle other cases too - var prevContainer = Keyboard.FocusedElement as ConnectionContainer; - - if (_previousFocusNavigationDirection.HasValue && request.FocusNavigationDirection.IsOppositeOf(_previousFocusNavigationDirection.Value)) - { - // If the request is in the opposite direction of the last focus navigation, try to restore the previous focused container - if (_previousFocusedContainer.TryGetTarget(out var previousContainer) && previousContainer.Focus()) - { - _previousFocusNavigationDirection = request.FocusNavigationDirection; - if (prevContainer != null) - { - _previousFocusedContainer.SetTarget(prevContainer); - } - - // TODO: Bring into view? - return true; - } - } - else if (TryGetContainerToFocus(out var containerToFocus, request) && containerToFocus!.Focus()) - { - _previousFocusNavigationDirection = request.FocusNavigationDirection; - if (prevContainer != null) - { - _previousFocusedContainer.SetTarget(prevContainer); - } - - // TODO: Bring into view? - return true; - } - - return false; + return _focusNavigator.TryMoveFocus(request, TryFindContainerToFocus, target => Editor?.BringIntoView(target.Bounds)); } - private bool TryGetContainerToFocus(out ConnectionContainer? containerToFocus, TraversalRequest request) + private bool TryFindContainerToFocus(TraversalRequest request, out ConnectionContainer? containerToFocus) { containerToFocus = null; @@ -147,7 +115,7 @@ private bool TryGetContainerToFocus(out ConnectionContainer? containerToFocus, T protected virtual ConnectionContainer? FindNextFocusTarget(ConnectionContainer currentContainer, TraversalRequest request) { - var focusNavigator = new LinearFocusNavigator(ConnectionContainers); + var focusNavigator = new ConnectionFocusNavigator(ConnectionContainers); var result = focusNavigator.FindNextFocusTarget(currentContainer, request); return result?.Element; diff --git a/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs b/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs index bf125887..82bc0112 100644 --- a/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs +++ b/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs @@ -22,48 +22,16 @@ public partial class NodifyEditor : IKeyboardNavigationLayer, IKeyboardNavigatio int IReadOnlyCollection.Count => _navigationLayers.Count; - // TODO: When do we clear these? - private readonly WeakReference _previousFocusedContainer = new WeakReference(null); - private FocusNavigationDirection? _previousFocusNavigationDirection; - #region Focus Handling + private readonly StatefulFocusNavigator _focusNavigator = new StatefulFocusNavigator(); + bool IKeyboardNavigationLayer.TryMoveFocus(TraversalRequest request) { - // TODO: throw exception if request.FocusNavigationDirection is not directional (Left, Right, Up, Down) or handle other cases too - var prevContainer = Keyboard.FocusedElement as ItemContainer; - - if (_previousFocusNavigationDirection.HasValue && request.FocusNavigationDirection.IsOppositeOf(_previousFocusNavigationDirection.Value)) - { - // If the request is in the opposite direction of the last focus navigation, try to restore the previous focused container - if (_previousFocusedContainer.TryGetTarget(out var previousContainer) && previousContainer.Focus()) - { - _previousFocusNavigationDirection = request.FocusNavigationDirection; - if (prevContainer != null) - { - _previousFocusedContainer.SetTarget(prevContainer); - } - - BringIntoView(previousContainer, ItemContainer.BringIntoViewEdgeOffset); - return true; - } - } - else if (TryGetContainerToFocus(out var containerToFocus, request) && containerToFocus!.Focus()) - { - _previousFocusNavigationDirection = request.FocusNavigationDirection; - if (prevContainer != null) - { - _previousFocusedContainer.SetTarget(prevContainer); - } - - BringIntoView(containerToFocus, ItemContainer.BringIntoViewEdgeOffset); - return true; - } - - return false; + return _focusNavigator.TryMoveFocus(request, TryFindContainerToFocus, target => BringIntoView(target.Element, ItemContainer.BringIntoViewEdgeOffset)); } - private bool TryGetContainerToFocus(out ItemContainer? containerToFocus, TraversalRequest request) + private bool TryFindContainerToFocus(TraversalRequest request, out ItemContainer? containerToFocus) { containerToFocus = null; diff --git a/Nodify/Interactivity/KeyboardNavigation/ConnectionFocusNavigator.cs b/Nodify/Interactivity/KeyboardNavigation/ConnectionFocusNavigator.cs new file mode 100644 index 00000000..a59fead4 --- /dev/null +++ b/Nodify/Interactivity/KeyboardNavigation/ConnectionFocusNavigator.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Windows.Controls; +using System.Windows.Input; + +namespace Nodify.Interactivity +{ + internal readonly struct ConnectionFocusNavigator + where TElement : ConnectionContainer, IKeyboardFocusTarget + { + private readonly IEnumerable> _availableTargets; + + public ConnectionFocusNavigator(IEnumerable> availableTargets) + { + _availableTargets = availableTargets; + } + + public readonly IKeyboardFocusTarget? FindNextFocusTarget(IKeyboardFocusTarget currentContainer, TraversalRequest request) + { + if (currentContainer is ConnectionContainer container + && container.Connection is BaseConnection baseConnection + && CanNavigateDirectional(baseConnection, request.FocusNavigationDirection)) + { + var directionalFocusNavigator = new DirectionalFocusNavigator(_availableTargets); + return directionalFocusNavigator.FindNextFocusTarget(currentContainer, request); + } + + var linearFocusNavigator = new LinearFocusNavigator(_availableTargets); + return linearFocusNavigator.FindNextFocusTarget(currentContainer, request); + } + + private static bool CanNavigateDirectional(BaseConnection baseConnection, FocusNavigationDirection dir) + { + return (baseConnection.SourceOrientation == Orientation.Horizontal && IsVertical(dir)) + || (baseConnection.SourceOrientation == Orientation.Vertical && IsHorizontal(dir)); + } + + private static bool IsVertical(FocusNavigationDirection dir) + { + return dir == FocusNavigationDirection.Up || dir == FocusNavigationDirection.Down; + } + + private static bool IsHorizontal(FocusNavigationDirection dir) + { + return dir == FocusNavigationDirection.Left || dir == FocusNavigationDirection.Right; + } + } +} diff --git a/Nodify/Interactivity/KeyboardNavigation/DirectionalFocusNavigator.cs b/Nodify/Interactivity/KeyboardNavigation/DirectionalFocusNavigator.cs new file mode 100644 index 00000000..4ba14990 --- /dev/null +++ b/Nodify/Interactivity/KeyboardNavigation/DirectionalFocusNavigator.cs @@ -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 + where TElement : UIElement, IKeyboardFocusTarget + { + private readonly IEnumerable> _availableTargets; + + public DirectionalFocusNavigator(IEnumerable> availableTargets) + { + _availableTargets = availableTargets; + } + + public readonly IKeyboardFocusTarget? FindNextFocusTarget(IKeyboardFocusTarget currentContainer, TraversalRequest request) + { + var currentContainerBounds = currentContainer.Bounds; + + IEnumerable> candidates = request.FocusNavigationDirection switch + { + FocusNavigationDirection.Left => _availableTargets.Where(c => c.Bounds.Right < currentContainerBounds.Left), + FocusNavigationDirection.Right => _availableTargets.Where(c => c.Bounds.Left > currentContainerBounds.Right), + FocusNavigationDirection.Up => _availableTargets.Where(c => c.Bounds.Bottom < currentContainerBounds.Top), + FocusNavigationDirection.Down => _availableTargets.Where(c => c.Bounds.Top > currentContainerBounds.Bottom), + FocusNavigationDirection.Previous => FindCandidatesLinearly(currentContainer, request), + FocusNavigationDirection.Next => FindCandidatesLinearly(currentContainer, request), + FocusNavigationDirection.First => FindCandidatesLinearly(currentContainer, request), + FocusNavigationDirection.Last => FindCandidatesLinearly(currentContainer, request), + _ => Array.Empty>() + }; + + // 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>() + }; + + request.Wrapped = true; + } + + IKeyboardFocusTarget? 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 static IKeyboardFocusTarget[] FindCandidatesLinearly(IKeyboardFocusTarget currentContainer, TraversalRequest request) + { + var nextTarget = new LinearFocusNavigator().FindNextFocusTarget(currentContainer, request); + return nextTarget is null ? Array.Empty>() : new[] { nextTarget }; + } + } +} diff --git a/Nodify/Interactivity/IKeyboardNavigationLayer.cs b/Nodify/Interactivity/KeyboardNavigation/IKeyboardNavigationLayer.cs similarity index 87% rename from Nodify/Interactivity/IKeyboardNavigationLayer.cs rename to Nodify/Interactivity/KeyboardNavigation/IKeyboardNavigationLayer.cs index 8f8c0776..716af636 100644 --- a/Nodify/Interactivity/IKeyboardNavigationLayer.cs +++ b/Nodify/Interactivity/KeyboardNavigation/IKeyboardNavigationLayer.cs @@ -4,7 +4,6 @@ namespace Nodify.Interactivity { - public class KeyboardNavigationLayerId { public static readonly KeyboardNavigationLayerId Nodes = new KeyboardNavigationLayerId(); @@ -36,4 +35,11 @@ public interface IKeyboardNavigationLayer void OnActivate(); void OnDeactivate(); } + + public interface IKeyboardFocusTarget + where TElement : UIElement + { + Rect Bounds { get; } + TElement Element { get; } + } } diff --git a/Nodify/Interactivity/IKeyboardFocusTarget.cs b/Nodify/Interactivity/KeyboardNavigation/LinearFocusNavigator.cs similarity index 50% rename from Nodify/Interactivity/IKeyboardFocusTarget.cs rename to Nodify/Interactivity/KeyboardNavigation/LinearFocusNavigator.cs index b4084064..09e8486c 100644 --- a/Nodify/Interactivity/IKeyboardFocusTarget.cs +++ b/Nodify/Interactivity/KeyboardNavigation/LinearFocusNavigator.cs @@ -5,68 +5,6 @@ namespace Nodify.Interactivity { - public interface IKeyboardFocusTarget - where TElement : UIElement - { - Rect Bounds { get; } - TElement Element { get; } - } - - internal readonly struct DirectionalFocusNavigator - where TElement : UIElement, IKeyboardFocusTarget - { - private readonly IEnumerable> _availableTargets; - - public DirectionalFocusNavigator(IEnumerable> availableTargets) - { - _availableTargets = availableTargets; - } - - public readonly IKeyboardFocusTarget? FindNextFocusTarget(IKeyboardFocusTarget currentContainer, TraversalRequest request) - { - var currentContainerBounds = currentContainer.Bounds; - - IEnumerable> candidates = request.FocusNavigationDirection switch - { - FocusNavigationDirection.Left => _availableTargets.Where(c => c.Bounds.Right <= currentContainerBounds.Left), - FocusNavigationDirection.Right => _availableTargets.Where(c => c.Bounds.Left >= currentContainerBounds.Right), - FocusNavigationDirection.Up => _availableTargets.Where(c => c.Bounds.Bottom <= currentContainerBounds.Top), - FocusNavigationDirection.Down => _availableTargets.Where(c => c.Bounds.Top >= currentContainerBounds.Bottom), - _ => Enumerable.Empty>() - }; - - // 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), - _ => Enumerable.Empty>() - }; - - request.Wrapped = true; - } - - IKeyboardFocusTarget? 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; - } - } - internal readonly struct LinearFocusNavigator where TElement : UIElement, IKeyboardFocusTarget { @@ -87,14 +25,12 @@ public LinearFocusNavigator(IEnumerable> availabl public readonly IKeyboardFocusTarget? FindNextFocusTarget(IKeyboardFocusTarget currentContainer, TraversalRequest request) { - var currentContainerBounds = currentContainer.Bounds; - 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> ?? _availableTargets.ToList(); - var currentIndex = availableTargets.IndexOf(currentContainer); + int currentIndex = availableTargets.IndexOf(currentContainer); IKeyboardFocusTarget? candidate = direction switch { diff --git a/Nodify/Interactivity/KeyboardNavigation/StatefulFocusNavigator.cs b/Nodify/Interactivity/KeyboardNavigation/StatefulFocusNavigator.cs new file mode 100644 index 00000000..daabfdb6 --- /dev/null +++ b/Nodify/Interactivity/KeyboardNavigation/StatefulFocusNavigator.cs @@ -0,0 +1,44 @@ +using System; +using System.Windows.Input; +using System.Windows; + +namespace Nodify.Interactivity +{ + internal class StatefulFocusNavigator + where TElement : UIElement, IKeyboardFocusTarget + { + public delegate bool FindNextFocusTargetDelegate(TraversalRequest request, out TElement? elementToFocus); + + // TODO: When do we clear these? + private readonly WeakReference _previousFocusedContainer = new WeakReference(null); + private FocusNavigationDirection? _previousFocusNavigationDirection; + + public bool TryMoveFocus(TraversalRequest request, FindNextFocusTargetDelegate findNext, Action> onFocus) + { + 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 (_previousFocusedContainer.TryGetTarget(out var prevTarget) + && _previousFocusNavigationDirection.HasValue + && request.FocusNavigationDirection.IsOppositeOf(_previousFocusNavigationDirection.Value) + && prevTarget!.Focus()) + { + _previousFocusNavigationDirection = request.FocusNavigationDirection; + _previousFocusedContainer.SetTarget(currentTarget); + + onFocus(prevTarget); + return true; + } + else if (findNext(request, out var nextTarget) && nextTarget!.Element.Focus()) + { + _previousFocusNavigationDirection = request.FocusNavigationDirection; + _previousFocusedContainer.SetTarget(currentTarget); + + onFocus(nextTarget); + return true; + } + + return false; + } + } +} diff --git a/Nodify/Utilities/EditorGesturesExtensions.cs b/Nodify/Utilities/EditorGesturesExtensions.cs index d6e63d47..c6ea1527 100644 --- a/Nodify/Utilities/EditorGesturesExtensions.cs +++ b/Nodify/Utilities/EditorGesturesExtensions.cs @@ -64,13 +64,16 @@ public static bool TryGetNavigationDirection(this EditorGestures.DirectionalNavi return x != 0 || y != 0; } - // TODO: Handle all cases? public static bool IsOppositeOf(this FocusNavigationDirection direction, FocusNavigationDirection other) { - return (direction == FocusNavigationDirection.Left && other == FocusNavigationDirection.Right) || - (direction == FocusNavigationDirection.Right && other == FocusNavigationDirection.Left) || - (direction == FocusNavigationDirection.Up && other == FocusNavigationDirection.Down) || - (direction == FocusNavigationDirection.Down && other == FocusNavigationDirection.Up); + return (direction == FocusNavigationDirection.Left && other == FocusNavigationDirection.Right) + || (direction == FocusNavigationDirection.Right && other == FocusNavigationDirection.Left) + || (direction == FocusNavigationDirection.Up && other == FocusNavigationDirection.Down) + || (direction == FocusNavigationDirection.Down && other == FocusNavigationDirection.Up) + || (direction == FocusNavigationDirection.Next && other == FocusNavigationDirection.Previous) + || (direction == FocusNavigationDirection.Previous && other == FocusNavigationDirection.Next) + || (direction == FocusNavigationDirection.First && other == FocusNavigationDirection.Last) + || (direction == FocusNavigationDirection.Last && other == FocusNavigationDirection.First); } } } From 3655f14b277f383409c6019ffb70f0c2f508d026 Mon Sep 17 00:00:00 2001 From: miroiu Date: Wed, 11 Jun 2025 23:52:00 +0300 Subject: [PATCH 06/28] Add focus visual to connections --- .../Editor/NodifyEditorView.xaml | 6 +++ Examples/Nodify.Playground/EditorSettings.cs | 33 +++++++++++++ Nodify/Connections/BaseConnection.cs | 48 ++++++++++++++++++- Nodify/Connections/ConnectionContainer.cs | 4 +- .../Connections/ConnectionsMultiSelector.cs | 2 +- .../ConnectionFocusNavigator.cs | 47 ------------------ 6 files changed, 88 insertions(+), 52 deletions(-) delete mode 100644 Nodify/Interactivity/KeyboardNavigation/ConnectionFocusNavigator.cs diff --git a/Examples/Nodify.Playground/Editor/NodifyEditorView.xaml b/Examples/Nodify.Playground/Editor/NodifyEditorView.xaml index eb514c82..34cfcab8 100644 --- a/Examples/Nodify.Playground/Editor/NodifyEditorView.xaml +++ b/Examples/Nodify.Playground/Editor/NodifyEditorView.xaml @@ -175,6 +175,12 @@ Value="{Binding SelectableConnections, Source={x:Static local:EditorSettings.Instance}}" /> + + + diff --git a/Examples/Nodify.Playground/EditorSettings.cs b/Examples/Nodify.Playground/EditorSettings.cs index aac5d31b..7bd89785 100644 --- a/Examples/Nodify.Playground/EditorSettings.cs +++ b/Examples/Nodify.Playground/EditorSettings.cs @@ -193,6 +193,18 @@ private EditorSettings() () => Instance.ConnectionTargetOffset, val => Instance.ConnectionTargetOffset = val, "Connection target offset: "), + new ProxySettingViewModel( + () => Instance.ConnectionStrokeThickness, + val => Instance.ConnectionStrokeThickness = val, + "Connection stroke thickness: "), + new ProxySettingViewModel( + () => Instance.ConnectionOutlineThickness, + val => Instance.ConnectionOutlineThickness = val, + "Connection outline thickness: "), + new ProxySettingViewModel( + () => Instance.ConnectionFocusVisualPadding, + val => Instance.ConnectionFocusVisualPadding = val, + "Connection focus visual padding: "), new ProxySettingViewModel( () => Instance.DisplayConnectionsOnTop, val => Instance.DisplayConnectionsOnTop = val, @@ -614,6 +626,27 @@ public PointEditor ConnectionTargetOffset set => SetProperty(ref _connectionTargetOffset, value); } + private double _connectionStrokeThickness = 3; + public double ConnectionStrokeThickness + { + get => _connectionStrokeThickness; + set => SetProperty(ref _connectionStrokeThickness, value); + } + + private double _connectionOutlineThickness = 5; + public double ConnectionOutlineThickness + { + get => _connectionOutlineThickness; + set => SetProperty(ref _connectionOutlineThickness, value); + } + + private double _connectionFocusVisualPadding = 1; + public double ConnectionFocusVisualPadding + { + get => _connectionFocusVisualPadding; + set => SetProperty(ref _connectionFocusVisualPadding, value); + } + private uint _directionalArrowsCount = 3; public uint DirectionalArrowsCount { diff --git a/Nodify/Connections/BaseConnection.cs b/Nodify/Connections/BaseConnection.cs index d053a2a8..3819a013 100644 --- a/Nodify/Connections/BaseConnection.cs +++ b/Nodify/Connections/BaseConnection.cs @@ -134,6 +134,8 @@ public abstract class BaseConnection : Shape, IKeyboardFocusTarget SetValue(OutlineBrushProperty, value); } + /// + /// The pen used to render the focus visual. + /// + public Pen? FocusVisualPen + { + get => (Pen?)GetValue(FocusVisualPenProperty); + set => SetValue(FocusVisualPenProperty, value); + } + + /// + /// The space between the focus visual and the connection geometry. + /// + public double FocusVisualPadding + { + get => (double)GetValue(FocusVisualPaddingProperty); + set => SetValue(FocusVisualPaddingProperty, value); + } + /// /// The brush used to render the . /// @@ -492,10 +512,30 @@ public event ConnectionEventHandler Split protected static readonly Vector ZeroVector = new Vector(0d, 0d); // Use Source for both corners to ensure Top-Left aligns with intended keyboard focus point. - Rect IKeyboardFocusTarget.Bounds => new Rect(Source, Source); + Rect IKeyboardFocusTarget.Bounds + => new Rect(Direction == ConnectionDirection.Forward ? Target : Source, Direction == ConnectionDirection.Forward ? Target : Source); + FrameworkElement IKeyboardFocusTarget.Element => this; private Pen? _outlinePen; + private static Pen? _defaultFocusVisualPen; + + private static Pen DefaultFocusVisualPen + { + get + { + if (_defaultFocusVisualPen is null) + { + _defaultFocusVisualPen = new Pen(SystemColors.ControlTextBrush, 1) + { + DashStyle = new DashStyle { Dashes = { 0.5d, 3d } } + }; + _defaultFocusVisualPen.Freeze(); + } + + return _defaultFocusVisualPen; + } + } private readonly StreamGeometry _geometry = new StreamGeometry { @@ -913,6 +953,12 @@ protected override void OnRender(DrawingContext drawingContext) drawingContext.DrawGeometry(OutlineBrush, GetOutlinePen(), DefiningGeometry); } + if (FocusVisualPen != null && Container is { IsKeyboardFocused: true }) + { + var widenPen = new Pen(null, StrokeThickness + FocusVisualPen.Thickness + FocusVisualPadding * 2d); + drawingContext.DrawGeometry(null, FocusVisualPen, DefiningGeometry.GetWidenedPathGeometry(widenPen)); + } + base.OnRender(drawingContext); if (!string.IsNullOrEmpty(Text)) diff --git a/Nodify/Connections/ConnectionContainer.cs b/Nodify/Connections/ConnectionContainer.cs index d88e6c02..c3792595 100644 --- a/Nodify/Connections/ConnectionContainer.cs +++ b/Nodify/Connections/ConnectionContainer.cs @@ -97,9 +97,7 @@ internal ConnectionContainer(ConnectionsMultiSelector selector) protected override void OnIsKeyboardFocusedChanged(DependencyPropertyChangedEventArgs e) { - // TODO: Custom property on BaseConnection - BaseConnection.SetIsSelected(Connection, (bool)e.NewValue); - //Connection?.SetValue(IsKeyboardFocusedProperty, e.NewValue); + Connection?.InvalidateVisual(); } /// diff --git a/Nodify/Connections/ConnectionsMultiSelector.cs b/Nodify/Connections/ConnectionsMultiSelector.cs index 18a06bae..5f8c6318 100644 --- a/Nodify/Connections/ConnectionsMultiSelector.cs +++ b/Nodify/Connections/ConnectionsMultiSelector.cs @@ -115,7 +115,7 @@ private bool TryFindContainerToFocus(TraversalRequest request, out ConnectionCon protected virtual ConnectionContainer? FindNextFocusTarget(ConnectionContainer currentContainer, TraversalRequest request) { - var focusNavigator = new ConnectionFocusNavigator(ConnectionContainers); + var focusNavigator = new DirectionalFocusNavigator(ConnectionContainers); var result = focusNavigator.FindNextFocusTarget(currentContainer, request); return result?.Element; diff --git a/Nodify/Interactivity/KeyboardNavigation/ConnectionFocusNavigator.cs b/Nodify/Interactivity/KeyboardNavigation/ConnectionFocusNavigator.cs deleted file mode 100644 index a59fead4..00000000 --- a/Nodify/Interactivity/KeyboardNavigation/ConnectionFocusNavigator.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Generic; -using System.Windows.Controls; -using System.Windows.Input; - -namespace Nodify.Interactivity -{ - internal readonly struct ConnectionFocusNavigator - where TElement : ConnectionContainer, IKeyboardFocusTarget - { - private readonly IEnumerable> _availableTargets; - - public ConnectionFocusNavigator(IEnumerable> availableTargets) - { - _availableTargets = availableTargets; - } - - public readonly IKeyboardFocusTarget? FindNextFocusTarget(IKeyboardFocusTarget currentContainer, TraversalRequest request) - { - if (currentContainer is ConnectionContainer container - && container.Connection is BaseConnection baseConnection - && CanNavigateDirectional(baseConnection, request.FocusNavigationDirection)) - { - var directionalFocusNavigator = new DirectionalFocusNavigator(_availableTargets); - return directionalFocusNavigator.FindNextFocusTarget(currentContainer, request); - } - - var linearFocusNavigator = new LinearFocusNavigator(_availableTargets); - return linearFocusNavigator.FindNextFocusTarget(currentContainer, request); - } - - private static bool CanNavigateDirectional(BaseConnection baseConnection, FocusNavigationDirection dir) - { - return (baseConnection.SourceOrientation == Orientation.Horizontal && IsVertical(dir)) - || (baseConnection.SourceOrientation == Orientation.Vertical && IsHorizontal(dir)); - } - - private static bool IsVertical(FocusNavigationDirection dir) - { - return dir == FocusNavigationDirection.Up || dir == FocusNavigationDirection.Down; - } - - private static bool IsHorizontal(FocusNavigationDirection dir) - { - return dir == FocusNavigationDirection.Left || dir == FocusNavigationDirection.Right; - } - } -} From a6f7e076dee791c25389f7006379f0c3a729deca Mon Sep 17 00:00:00 2001 From: miroiu Date: Thu, 12 Jun 2025 00:10:16 +0300 Subject: [PATCH 07/28] Add connection focus visual pen resource key --- Examples/Nodify.Playground/App.xaml | 7 +++++++ Nodify/Connections/BaseConnection.cs | 16 +++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Examples/Nodify.Playground/App.xaml b/Examples/Nodify.Playground/App.xaml index 00eb1241..d8307c44 100644 --- a/Examples/Nodify.Playground/App.xaml +++ b/Examples/Nodify.Playground/App.xaml @@ -1,6 +1,7 @@  @@ -24,6 +25,12 @@ + + + diff --git a/Nodify/Connections/BaseConnection.cs b/Nodify/Connections/BaseConnection.cs index 3819a013..c0fcbb73 100644 --- a/Nodify/Connections/BaseConnection.cs +++ b/Nodify/Connections/BaseConnection.cs @@ -517,6 +517,11 @@ Rect IKeyboardFocusTarget.Bounds FrameworkElement IKeyboardFocusTarget.Element => this; + /// + /// The key used to retrieve the resource. + /// + public static ResourceKey FocusVisualPenKey { get; } = new ComponentResourceKey(typeof(BaseConnection), nameof(FocusVisualPen)); + private Pen? _outlinePen; private static Pen? _defaultFocusVisualPen; @@ -953,10 +958,15 @@ protected override void OnRender(DrawingContext drawingContext) drawingContext.DrawGeometry(OutlineBrush, GetOutlinePen(), DefiningGeometry); } - if (FocusVisualPen != null && Container is { IsKeyboardFocused: true }) + if (Container is { IsKeyboardFocused: true }) { - var widenPen = new Pen(null, StrokeThickness + FocusVisualPen.Thickness + FocusVisualPadding * 2d); - drawingContext.DrawGeometry(null, FocusVisualPen, DefiningGeometry.GetWidenedPathGeometry(widenPen)); + // TODO: May want to cache the result of FindResource somewhere + var drawPen = (FocusVisualPen == DefaultFocusVisualPen + ? TryFindResource(FocusVisualPenKey) as Pen + : FocusVisualPen) ?? DefaultFocusVisualPen; + + var widenPen = new Pen(null, StrokeThickness + drawPen.Thickness + FocusVisualPadding * 2d); + drawingContext.DrawGeometry(null, drawPen, DefiningGeometry.GetWidenedPathGeometry(widenPen)); } base.OnRender(drawingContext); From fdfe2044774c77e79634d0394ad838f665098800 Mon Sep 17 00:00:00 2001 From: miroiu Date: Thu, 12 Jun 2025 17:16:13 +0300 Subject: [PATCH 08/28] Add default focus visual --- Examples/Nodify.Playground/App.xaml | 1 + .../Connections/ConnectionsMultiSelector.cs | 2 +- Nodify/Containers/ItemContainer.cs | 5 --- .../Editor/NodifyEditor.KeyboardNavigation.cs | 2 +- Nodify/Editor/NodifyEditor.cs | 32 +++++++++++-------- Nodify/Themes/Controls.xaml | 1 + Nodify/Themes/FocusVisual.xaml | 15 +++++++++ 7 files changed, 37 insertions(+), 21 deletions(-) create mode 100644 Nodify/Themes/FocusVisual.xaml diff --git a/Examples/Nodify.Playground/App.xaml b/Examples/Nodify.Playground/App.xaml index d8307c44..56ba95a6 100644 --- a/Examples/Nodify.Playground/App.xaml +++ b/Examples/Nodify.Playground/App.xaml @@ -11,6 +11,7 @@ + + + + + diff --git a/Examples/Nodify.Calculator/EditorView.xaml b/Examples/Nodify.Calculator/EditorView.xaml index 24f923a0..6b39bae1 100644 --- a/Examples/Nodify.Calculator/EditorView.xaml +++ b/Examples/Nodify.Calculator/EditorView.xaml @@ -235,6 +235,11 @@ Command="{Binding GroupSelectionCommand}" /> + + + + @@ -251,7 +256,8 @@ - + @@ -353,8 +359,13 @@ - + + + + + @@ -371,6 +382,11 @@ Command="{Binding GroupSelectionCommand}" /> + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Nodify.Shapes/Canvas/CanvasView.xaml b/Examples/Nodify.Shapes/Canvas/CanvasView.xaml index 7c5186bd..ee715176 100644 --- a/Examples/Nodify.Shapes/Canvas/CanvasView.xaml +++ b/Examples/Nodify.Shapes/Canvas/CanvasView.xaml @@ -70,6 +70,7 @@ + GridCellSize="5" + Focusable="False"> @@ -414,6 +419,7 @@ @@ -482,8 +488,9 @@ - - + + - + + SelectedValue="{Binding CanvasToolbar.SelectedTool}" + KeyboardNavigation.ControlTabNavigation="None"> - + - + - + diff --git a/Examples/Nodify.Shapes/MainWindow.xaml.cs b/Examples/Nodify.Shapes/MainWindow.xaml.cs index 43014701..c126ba7b 100644 --- a/Examples/Nodify.Shapes/MainWindow.xaml.cs +++ b/Examples/Nodify.Shapes/MainWindow.xaml.cs @@ -1,4 +1,5 @@ using System.Windows; +using System.Windows.Input; namespace Nodify.Shapes { @@ -10,6 +11,16 @@ public partial class MainWindow : Window public MainWindow() { InitializeComponent(); + + EventManager.RegisterClassHandler( + typeof(UIElement), + Keyboard.PreviewGotKeyboardFocusEvent, + (KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus); + } + + private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) + { + Title = e.NewFocus.ToString(); } } } \ No newline at end of file From fff6558bfc595152a765b2dcaa62c9870d981812 Mon Sep 17 00:00:00 2001 From: miroiu Date: Mon, 16 Jun 2025 23:36:29 +0300 Subject: [PATCH 19/28] Add keyboard navigation to decorators layer --- .../Connections/ConnectionsMultiSelector.cs | 31 +++-- Nodify/Containers/DecoratorContainer.cs | 42 +++++- Nodify/Containers/DecoratorsControl.cs | 120 +++++++++++++++++- .../Editor/NodifyEditor.KeyboardNavigation.cs | 5 + Nodify/Interactivity/Gestures/MouseGesture.cs | 3 +- Nodify/Interactivity/InputElementState.cs | 3 +- Nodify/Nodes/Node.cs | 1 - Nodify/Utilities/SelectionHelper.cs | 4 +- 8 files changed, 179 insertions(+), 30 deletions(-) diff --git a/Nodify/Connections/ConnectionsMultiSelector.cs b/Nodify/Connections/ConnectionsMultiSelector.cs index d1674b55..c29a342b 100644 --- a/Nodify/Connections/ConnectionsMultiSelector.cs +++ b/Nodify/Connections/ConnectionsMultiSelector.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Diagnostics; using System.Linq; using System.Windows; using System.Windows.Controls; @@ -92,6 +91,21 @@ public ConnectionsMultiSelector() _focusNavigator = new StatefulFocusNavigator(target => Editor?.BringIntoView(target.Bounds, NodifyEditor.BringIntoViewEdgeOffset)); } + protected override DependencyObject GetContainerForItemOverride() + => new ConnectionContainer(this); + + protected override bool IsItemItsOwnContainerOverride(object item) + => item is ConnectionContainer; + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + Editor = this.GetParentOfType(); + + Editor?.RegisterNavigationLayer(this); + } + #region Keyboard Navigation public KeyboardNavigationLayerId Id { get; } = KeyboardNavigationLayerId.Connections; @@ -151,12 +165,6 @@ void IKeyboardNavigationLayer.OnDeactivated() #endregion - protected override DependencyObject GetContainerForItemOverride() - => new ConnectionContainer(this); - - protected override bool IsItemItsOwnContainerOverride(object item) - => item is ConnectionContainer; - public void Select(ConnectionContainer container) { BeginUpdateSelectedItems(); @@ -177,15 +185,6 @@ public void Select(ConnectionContainer container) Editor?.UnselectAll(); } - public override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - Editor = this.GetParentOfType(); - - Editor?.RegisterNavigationLayer(this); - } - #region Selection Handlers private void OnSelectedItemsSourceChanged(IList oldValue, IList newValue) diff --git a/Nodify/Containers/DecoratorContainer.cs b/Nodify/Containers/DecoratorContainer.cs index 5416774f..10941306 100644 --- a/Nodify/Containers/DecoratorContainer.cs +++ b/Nodify/Containers/DecoratorContainer.cs @@ -1,12 +1,15 @@ -using System.Windows; +using Nodify.Interactivity; +using System.Windows; using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; namespace Nodify { /// /// The container for all the items generated from the collection. /// - public class DecoratorContainer : ContentControl, INodifyCanvasItem + public class DecoratorContainer : ContentControl, INodifyCanvasItem, IKeyboardFocusTarget { #region Dependency Properties @@ -62,15 +65,48 @@ protected void OnLocationChanged() #endregion + public Rect Bounds => new Rect(Location, ActualSize); + DecoratorContainer IKeyboardFocusTarget.Element => this; + + private DecoratorsControl? _owner; + public DecoratorsControl? Owner => _owner ??= this.GetParentOfType(); + static DecoratorContainer() { DefaultStyleKeyProperty.OverrideMetadata(typeof(DecoratorContainer), new FrameworkPropertyMetadata(typeof(DecoratorContainer))); + FocusableProperty.OverrideMetadata(typeof(DecoratorContainer), new FrameworkPropertyMetadata(BoxValue.True)); + + KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(DecoratorContainer), new FrameworkPropertyMetadata(KeyboardNavigationMode.Cycle)); + KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(DecoratorContainer), new FrameworkPropertyMetadata(KeyboardNavigationMode.Cycle)); + } + + public DecoratorContainer(DecoratorsControl parent) + { + _owner = parent; + } + + public DecoratorContainer() + { } /// protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) { - ActualSize = sizeInfo.NewSize; + SetCurrentValue(ActualSizeProperty, sizeInfo.NewSize); + } + + protected override void OnVisualParentChanged(DependencyObject oldParent) + { + if (VisualTreeHelper.GetParent(this) == null && IsKeyboardFocusWithin) + { + base.OnVisualParentChanged(oldParent); + + Owner?.Editor?.Focus(); + } + else + { + base.OnVisualParentChanged(oldParent); + } } } } diff --git a/Nodify/Containers/DecoratorsControl.cs b/Nodify/Containers/DecoratorsControl.cs index 5d9bef90..0dca56ab 100644 --- a/Nodify/Containers/DecoratorsControl.cs +++ b/Nodify/Containers/DecoratorsControl.cs @@ -1,19 +1,133 @@ -using System.Windows; +using Nodify.Interactivity; +using System.Collections.Generic; +using System.Linq; +using System.Windows; using System.Windows.Controls; +using System.Windows.Input; namespace Nodify { /// /// An that works with s. /// - internal sealed class DecoratorsControl : ItemsControl + public class DecoratorsControl : ItemsControl, IKeyboardNavigationLayer { + /// + /// Gets the that owns this . + /// + public NodifyEditor? Editor { get; private set; } + + /// + /// Gets a list of all s. + /// + /// Cache the result before using it to avoid extra allocations. + protected internal IReadOnlyCollection DecoratorContainers + { + get + { + ItemCollection items = Items; + var containers = new List(items.Count); + + for (var i = 0; i < items.Count; i++) + { + containers.Add((DecoratorContainer)ItemContainerGenerator.ContainerFromIndex(i)); + } + + return containers; + } + } + + static DecoratorsControl() + { + FocusableProperty.OverrideMetadata(typeof(DecoratorsControl), new FrameworkPropertyMetadata(BoxValue.False)); + + KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(DecoratorsControl), new FrameworkPropertyMetadata(KeyboardNavigationMode.None)); + KeyboardNavigation.ControlTabNavigationProperty.OverrideMetadata(typeof(DecoratorsControl), new FrameworkPropertyMetadata(KeyboardNavigationMode.None)); + KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(DecoratorsControl), new FrameworkPropertyMetadata(KeyboardNavigationMode.None)); + } + + public DecoratorsControl() + { + _focusNavigator = new StatefulFocusNavigator(target => Editor?.BringIntoView(target.Bounds, NodifyEditor.BringIntoViewEdgeOffset)); + } + /// protected override bool IsItemItsOwnContainerOverride(object item) => item is DecoratorContainer; /// protected override DependencyObject GetContainerForItemOverride() - => new DecoratorContainer(); + => new DecoratorContainer(this); + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + Editor = this.GetParentOfType(); + + if (NodifyEditor.AutoRegisterDecoratorsLayer) + { + Editor?.RegisterNavigationLayer(this); + } + } + + #region Keyboard Navigation + + public KeyboardNavigationLayerId Id { get; } = KeyboardNavigationLayerId.Decorators; + public IKeyboardFocusTarget? LastFocusedElement => _focusNavigator.LastFocusedElement; + + private readonly StatefulFocusNavigator _focusNavigator; + + public bool TryMoveFocus(TraversalRequest request) + { + return _focusNavigator.TryMoveFocus(request, TryFindContainerToFocus); + } + + public bool TryRestoreFocus() + { + return _focusNavigator.TryRestoreFocus(); + } + + private bool TryFindContainerToFocus(DecoratorContainer? currentElement, TraversalRequest request, out DecoratorContainer? containerToFocus) + { + containerToFocus = null; + + if (currentElement is DecoratorContainer focusedContainer) + { + containerToFocus = FindNextFocusTarget(focusedContainer, request); + } + else if (currentElement is UIElement elem && elem.GetParentOfType() is DecoratorContainer parentContainer) + { + containerToFocus = parentContainer; + } + else if (Items.Count > 0 && Editor != null) + { + var viewport = new Rect(Editor.ViewportLocation, Editor.ViewportSize); + var containers = DecoratorContainers; + containerToFocus = containers.FirstOrDefault(container => viewport.IntersectsWith(((IKeyboardFocusTarget)container).Bounds)) + ?? containers.First(); + } + + return containerToFocus != null; + } + + protected virtual DecoratorContainer? FindNextFocusTarget(DecoratorContainer currentContainer, TraversalRequest request) + { + var focusNavigator = new DirectionalFocusNavigator(DecoratorContainers); + var result = focusNavigator.FindNextFocusTarget(currentContainer, request); + + return result?.Element; + } + + void IKeyboardNavigationLayer.OnActivated() + { + TryRestoreFocus(); + } + + void IKeyboardNavigationLayer.OnDeactivated() + { + } + + #endregion } } diff --git a/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs b/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs index de64a9ea..24ed72f4 100644 --- a/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs +++ b/Nodify/Editor/NodifyEditor.KeyboardNavigation.cs @@ -21,6 +21,11 @@ public partial class NodifyEditor : IKeyboardNavigationLayer, IKeyboardNavigatio /// public static bool AutoFocusFirstElement { get; set; } = true; + /// + /// Automatically registers the decorators layer for keyboard navigation. + /// + public static bool AutoRegisterDecoratorsLayer { get; set; } + /// /// Indicates whether the viewport should automatically pan to follow elements moved via keyboard dragging. /// diff --git a/Nodify/Interactivity/Gestures/MouseGesture.cs b/Nodify/Interactivity/Gestures/MouseGesture.cs index 4d9450e7..5a66abcd 100644 --- a/Nodify/Interactivity/Gestures/MouseGesture.cs +++ b/Nodify/Interactivity/Gestures/MouseGesture.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Windows.Input; namespace Nodify.Interactivity diff --git a/Nodify/Interactivity/InputElementState.cs b/Nodify/Interactivity/InputElementState.cs index c6e4400f..4c63b998 100644 --- a/Nodify/Interactivity/InputElementState.cs +++ b/Nodify/Interactivity/InputElementState.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using System.Windows; +using System.Windows; using System.Windows.Input; namespace Nodify.Interactivity diff --git a/Nodify/Nodes/Node.cs b/Nodify/Nodes/Node.cs index b97c8599..f95d8693 100644 --- a/Nodify/Nodes/Node.cs +++ b/Nodify/Nodes/Node.cs @@ -3,7 +3,6 @@ using System.Collections.Specialized; using System.Windows; using System.Windows.Controls; -using System.Windows.Input; using System.Windows.Media; namespace Nodify diff --git a/Nodify/Utilities/SelectionHelper.cs b/Nodify/Utilities/SelectionHelper.cs index cfc3baaf..1d661ab2 100644 --- a/Nodify/Utilities/SelectionHelper.cs +++ b/Nodify/Utilities/SelectionHelper.cs @@ -1,9 +1,7 @@ -using Nodify.Interactivity; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Windows; -using System.Windows.Input; namespace Nodify { From 111bd1fb48bf87b7d450bccfdcb29cde3ea92aba Mon Sep 17 00:00:00 2001 From: miroiu Date: Tue, 17 Jun 2025 18:51:51 +0300 Subject: [PATCH 20/28] Added keyboard navigation to the minimap control Related #89 --- CHANGELOG.md | 9 +- Examples/Nodify.Shapes/Canvas/CanvasView.xaml | 2 +- .../Connections/ConnectionsMultiSelector.cs | 5 +- Nodify/Editor/EditorCommands.cs | 4 +- .../Editor/NodifyEditor.KeyboardNavigation.cs | 5 + Nodify/Editor/NodifyEditor.cs | 38 ++++++-- Nodify/Editor/States/KeyboardNavigation.cs | 6 +- .../Interactivity/Gestures/EditorGestures.cs | 39 +++++++- Nodify/Minimap/Minimap.cs | 93 ++++++++++++++++--- Nodify/Minimap/States/KeyboardNavigation.cs | 35 +++++++ Nodify/Minimap/States/MinimapState.cs | 1 + Nodify/Minimap/States/Zooming.cs | 19 ++++ Nodify/Themes/Styles/Minimap.xaml | 3 +- .../Utilities/DependencyObjectExtensions.cs | 24 +++++ 14 files changed, 248 insertions(+), 35 deletions(-) create mode 100644 Nodify/Minimap/States/KeyboardNavigation.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 33f63984..eeedfae0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,19 @@ > - Breaking Changes: > - Added ProcessHandledEvents to IInputHandler and removed it from InputProcessor +> - Renamed EditorGestures.Editor.ResetViewportLocation to EditorGestures.Editor.ResetViewport > - Features: > - Introduced a new BringIntoView method overload in NodifyEditor that accepts an offset from the viewport edges > - Added BringIntoViewEdgeOffset to NodifyEditor to control the viewport edge offset when bringing the focused element into view +> - Added ResetViewport to NodifyEditor to reset the viewport's location and zoom > - Improved tab and directional navigation, ensuring that focused elements are automatically brought into view > - Added keyboard navigation layers for nodes, connections and decorators; restricting keyboard navigation to the active layer -> - Added new gestures for keyboard navigation available in EditorGestures.Keyboard -> - Added NodifyEditor.AutoFocusFirstElement to control whether to automatically focus the first element when the navigation layer changes or the editor gets focused +> - Implemented IKeyboardNavigationLayerGroup in NodifyEditor for keyboard layers management +> - Added AutoRegisterConnectionsLayer, AutoRegisterDecoratorsLayer, AutoFocusFirstElement, PanViewportOnKeyboardDrag and MinimumNavigationStepSize to NodifyEditor +> - Added new gestures for keyboard navigation available in EditorGestures.Editor.Keyboard > - Added ToggleContentSelection to GroupingNode to toggle the selection of nodes inside the group +> - Added ZoomIn, ZoomOut and ResetViewport methods to the Minimap control and the corresponding gestures to EditorGestures.Minimap +> - Added NavigationStepSize static property to Minimap > - Bugfixes: #### **Version 7.0.4** diff --git a/Examples/Nodify.Shapes/Canvas/CanvasView.xaml b/Examples/Nodify.Shapes/Canvas/CanvasView.xaml index ee715176..60b0b75a 100644 --- a/Examples/Nodify.Shapes/Canvas/CanvasView.xaml +++ b/Examples/Nodify.Shapes/Canvas/CanvasView.xaml @@ -383,6 +383,7 @@ MinWidth="250" MinHeight="150" BorderBrush="{x:Null}" + Focusable="False" Margin="20"> - + + + - - - - diff --git a/Examples/Nodify.Shared/ThemeManager.cs b/Examples/Nodify.Shared/ThemeManager.cs index fd1cbcc6..ab84be50 100644 --- a/Examples/Nodify.Shared/ThemeManager.cs +++ b/Examples/Nodify.Shared/ThemeManager.cs @@ -115,7 +115,7 @@ public static void SetTheme(string themeName) { foreach (var res in resources) { - Application.Current.Resources.MergedDictionaries.Add(res); + Application.Current.Resources.MergedDictionaries.Insert(0, res); } // Unload current theme diff --git a/Nodify/Connections/BaseConnection.cs b/Nodify/Connections/BaseConnection.cs index 1c54b5a8..109a4df5 100644 --- a/Nodify/Connections/BaseConnection.cs +++ b/Nodify/Connections/BaseConnection.cs @@ -135,7 +135,7 @@ public abstract class BaseConnection : Shape, IKeyboardFocusTarget + xmlns:o="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options" + xmlns:local="clr-namespace:Nodify"> + + + + + + + + diff --git a/Nodify/Themes/Controls.xaml b/Nodify/Themes/Controls.xaml index 85ec540f..e58c9f0a 100644 --- a/Nodify/Themes/Controls.xaml +++ b/Nodify/Themes/Controls.xaml @@ -4,7 +4,6 @@ - diff --git a/Nodify/Themes/FocusVisual.xaml b/Nodify/Themes/FocusVisual.xaml index b6cd7efd..0f189823 100644 --- a/Nodify/Themes/FocusVisual.xaml +++ b/Nodify/Themes/FocusVisual.xaml @@ -3,10 +3,72 @@ xmlns:o="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options" xmlns:local="clr-namespace:Nodify"> + #FD5618 + + + + + + + + + + + + + + + + + + + + Brush="{StaticResource NodifyEditor.FocusVisualBrush}"> From 4932918427c5c726aca81f2f516cb015d23c4805 Mon Sep 17 00:00:00 2001 From: miroiu Date: Tue, 17 Jun 2025 20:06:27 +0300 Subject: [PATCH 22/28] Fix editor focus visual --- Examples/Nodify.Playground/MainWindow.xaml | 2 +- Nodify/Themes/Styles/NodifyEditor.xaml | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Examples/Nodify.Playground/MainWindow.xaml b/Examples/Nodify.Playground/MainWindow.xaml index 955e1a70..9c74ff0b 100644 --- a/Examples/Nodify.Playground/MainWindow.xaml +++ b/Examples/Nodify.Playground/MainWindow.xaml @@ -222,7 +222,7 @@ + + + + diff --git a/Examples/Nodify.Playground/SettingsView.xaml b/Examples/Nodify.Playground/SettingsView.xaml index d59375b5..aac6fa44 100644 --- a/Examples/Nodify.Playground/SettingsView.xaml +++ b/Examples/Nodify.Playground/SettingsView.xaml @@ -9,57 +9,76 @@ d:Foreground="{DynamicResource ForegroundBrush}" d:Background="{DynamicResource PanelBackgroundBrush}" mc:Ignorable="d"> - + - - + + - + - + - + - - + + - + - - + + + DodgerBlue + diff --git a/Examples/Nodify.Shapes/App.xaml b/Examples/Nodify.Shapes/App.xaml index 43a8dbfc..79ed23e3 100644 --- a/Examples/Nodify.Shapes/App.xaml +++ b/Examples/Nodify.Shapes/App.xaml @@ -6,6 +6,29 @@ + + + + + + + - - - - - diff --git a/Examples/Nodify.StateMachine/MainWindow.xaml b/Examples/Nodify.StateMachine/MainWindow.xaml index c80a03f5..803983c0 100644 --- a/Examples/Nodify.StateMachine/MainWindow.xaml +++ b/Examples/Nodify.StateMachine/MainWindow.xaml @@ -698,7 +698,8 @@ - + diff --git a/Nodify/Editor/NodifyEditor.cs b/Nodify/Editor/NodifyEditor.cs index 51f12feb..2d7a5585 100644 --- a/Nodify/Editor/NodifyEditor.cs +++ b/Nodify/Editor/NodifyEditor.cs @@ -961,12 +961,6 @@ protected override void OnKeyUp(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e) => InputProcessor.ProcessEvent(e); - protected override void OnPreviewKeyDown(KeyEventArgs e) - => InputProcessor.ProcessEvent(e); - - protected override void OnPreviewKeyUp(KeyEventArgs e) - => InputProcessor.ProcessEvent(e); - #endregion /// diff --git a/Nodify/Editor/States/KeyboardNavigation.cs b/Nodify/Editor/States/KeyboardNavigation.cs index 7b5a5b9e..a2898001 100644 --- a/Nodify/Editor/States/KeyboardNavigation.cs +++ b/Nodify/Editor/States/KeyboardNavigation.cs @@ -6,8 +6,15 @@ namespace Nodify.Interactivity { public static partial class EditorState { + /// + /// Represents the keyboard navigation state of the , allowing users to navigate and interact with nodes and connections using the keyboard. + /// public class KeyboardNavigation : InputElementState { + /// + /// Initializes a new instance of the class. + /// + /// The associated with this state. public KeyboardNavigation(NodifyEditor element) : base(element) { } diff --git a/Nodify/Interactivity/Gestures/KeyComboGesture.cs b/Nodify/Interactivity/Gestures/KeyComboGesture.cs index f9964efe..955f528f 100644 --- a/Nodify/Interactivity/Gestures/KeyComboGesture.cs +++ b/Nodify/Interactivity/Gestures/KeyComboGesture.cs @@ -7,7 +7,7 @@ namespace Nodify.Interactivity /// 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. /// - internal class KeyComboGesture : KeyGesture + public class KeyComboGesture : KeyGesture { private static readonly WeakReferenceCollection _allCombos = new WeakReferenceCollection(16); diff --git a/Nodify/Interactivity/KeyboardNavigation/IKeyboardNavigationLayer.cs b/Nodify/Interactivity/KeyboardNavigation/IKeyboardNavigationLayer.cs index 8a501199..0e871a3c 100644 --- a/Nodify/Interactivity/KeyboardNavigation/IKeyboardNavigationLayer.cs +++ b/Nodify/Interactivity/KeyboardNavigation/IKeyboardNavigationLayer.cs @@ -5,6 +5,9 @@ namespace Nodify.Interactivity { + /// + /// Represents a unique identifier for a keyboard navigation layer. + /// public class KeyboardNavigationLayerId { public static readonly KeyboardNavigationLayerId Nodes = new KeyboardNavigationLayerId(); @@ -12,39 +15,109 @@ public class KeyboardNavigationLayerId public static readonly KeyboardNavigationLayerId Decorators = new KeyboardNavigationLayerId(); } + /// + /// Represents a group of keyboard navigation layers that can be activated and navigated through. + /// public interface IKeyboardNavigationLayerGroup : IReadOnlyCollection { + /// + /// The current active keyboard navigation layer in the group, if any. + /// IKeyboardNavigationLayer? ActiveNavigationLayer { get; } + /// + /// Event that is raised when the active keyboard navigation layer changes. + /// event Action? ActiveNavigationLayerChanged; + /// + /// Activates the next keyboard navigation layer in the group, allowing focus to be restored to the last focused element in that layer. + /// + /// Returns true if the navigation layer was activated, false otherwise. bool ActivateNextNavigationLayer(); + /// + /// Activates the previous keyboard navigation layer in the group, allowing focus to be restored to the last focused element in that layer. + /// + /// Returns true if the navigation layer was activated, false otherwise. bool ActivatePreviousNavigationLayer(); + /// + /// Registers a new keyboard navigation layer to the group, allowing it to handle focus movement and restoration. + /// + /// The navigation layer. + /// bool RegisterNavigationLayer(IKeyboardNavigationLayer layer); + /// + /// Removes the specified keyboard navigation layer from the group. + /// + /// The navigation layer id. + /// Returns true if the layer was removed, false otherwise. bool RemoveNavigationLayer(KeyboardNavigationLayerId layerId); + /// + /// Activates the specified keyboard navigation layer, making it the active layer for focus management. + /// + /// The navigation layer id to activate. + /// Returns true if the navigation layer was activated, false otherwise. bool ActivateNavigationLayer(KeyboardNavigationLayerId layerId); } + /// + /// Represents a layer of keyboard navigation that can handle focus movement and restoration. + /// public interface IKeyboardNavigationLayer { + /// + /// Gets the unique identifier for this keyboard navigation layer. + /// KeyboardNavigationLayerId Id { get; } + + /// + /// Gets the last focused element within this layer, if any. + /// IKeyboardFocusTarget? LastFocusedElement { get; } + /// + /// Attempts to move focus within this layer based on the provided traversal request. + /// + /// The traversal request. + /// Returns true if the focus was moved, false otherwise. bool TryMoveFocus(TraversalRequest request); + + /// + /// Attempts to restore focus to the last focused element within this layer. + /// + /// Returns true if the focus was restored, false otherwise. bool TryRestoreFocus(); + /// + /// Called when the layer is activated, allowing for any necessary setup or focus management. + /// void OnActivated(); + + /// + /// Called when the layer is deactivated, allowing for any necessary cleanup or focus management. + /// void OnDeactivated(); } + /// + /// Represents a target for keyboard focus within a specific layer, providing bounds and the associated UI element. + /// + /// The associated UI element. public interface IKeyboardFocusTarget where TElement : UIElement { + /// + /// Gets the bounds of the focus target within the layer. + /// Rect Bounds { get; } + + /// + /// Gets the associated UI element for this focus target. + /// TElement Element { get; } } } diff --git a/Nodify/Minimap/Minimap.cs b/Nodify/Minimap/Minimap.cs index a0b91d51..4cfc1092 100644 --- a/Nodify/Minimap/Minimap.cs +++ b/Nodify/Minimap/Minimap.cs @@ -209,6 +209,18 @@ protected override void OnKeyUp(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e) => InputProcessor.ProcessEvent(e); + protected override void OnPreviewKeyDown(KeyEventArgs e) + => InputProcessor.ProcessEvent(e); + + protected override void OnPreviewKeyUp(KeyEventArgs e) + => InputProcessor.ProcessEvent(e); + + protected override void OnPreviewMouseDown(MouseButtonEventArgs e) + => InputProcessor.ProcessEvent(e); + + protected override void OnPreviewMouseUp(MouseButtonEventArgs e) + => InputProcessor.ProcessEvent(e); + #endregion #region Panning diff --git a/Nodify/Themes/Styles/DecoratorContainer.xaml b/Nodify/Themes/Styles/DecoratorContainer.xaml index 6ca87de7..78390c7d 100644 --- a/Nodify/Themes/Styles/DecoratorContainer.xaml +++ b/Nodify/Themes/Styles/DecoratorContainer.xaml @@ -5,8 +5,6 @@ - + + + + + + + DodgerBlue - - From fa9238738a265b64c56270cb70da28b5c9b382ec Mon Sep 17 00:00:00 2001 From: miroiu Date: Wed, 18 Jun 2025 18:25:36 +0300 Subject: [PATCH 28/28] Update api reference --- docs/api/API-Reference.md | 13 + docs/api/Nodify_BaseConnection.md | 40 ++- docs/api/Nodify_ConnectionContainer.md | 165 ++++++++++ docs/api/Nodify_ConnectionsMultiSelector.md | 184 +++++++++++ docs/api/Nodify_DecoratorContainer.md | 46 ++- docs/api/Nodify_DecoratorsControl.md | 142 ++++++++ docs/api/Nodify_GroupingNode.md | 10 + ...Interactivity_ConnectorState_Disconnect.md | 10 + .../Nodify_Interactivity_EditorGestures.md | 8 + ...ivity_EditorGestures_ConnectionGestures.md | 6 + ...tivity_EditorGestures_ConnectorGestures.md | 6 + ...rGestures_DirectionalNavigationGestures.md | 100 ++++++ ...ity_EditorGestures_GroupingNodeGestures.md | 18 +- ...ty_EditorGestures_ItemContainerGestures.md | 6 + ...activity_EditorGestures_MinimapGestures.md | 48 ++- ...ity_EditorGestures_NodifyEditorGestures.md | 32 +- ...activity_EditorState_KeyboardNavigation.md | 48 +++ .../api/Nodify_Interactivity_IInputHandler.md | 12 + ...activity_IKeyboardFocusTarget_TElement_.md | 32 ++ ..._Interactivity_IKeyboardNavigationLayer.md | 88 +++++ ...ractivity_IKeyboardNavigationLayerGroup.md | 120 +++++++ ...tivity_InputElementStateStack_TElement_.md | 10 + ...teractivity_InputElementState_TElement_.md | 10 + .../Nodify_Interactivity_InputGestureRef.md | 2 +- ...Interactivity_InputGestureRefExtensions.md | 34 ++ .../Nodify_Interactivity_InputProcessor.md | 12 - .../Nodify_Interactivity_KeyComboGesture.md | 107 ++++++ ...Interactivity_KeyboardNavigationLayerId.md | 56 ++++ ...ctivity_MinimapState_KeyboardNavigation.md | 38 +++ ...dify_Interactivity_MinimapState_Zooming.md | 10 + ...y_NodifyEditorGestures_KeyboardGestures.md | 112 +++++++ docs/api/Nodify_ItemContainer.md | 26 +- docs/api/Nodify_Minimap.md | 48 ++- docs/api/Nodify_NodifyEditor.md | 309 +++++++++++++++++- docs/api/Nodify_SelectionType.md | 2 +- 35 files changed, 1878 insertions(+), 32 deletions(-) create mode 100644 docs/api/Nodify_ConnectionContainer.md create mode 100644 docs/api/Nodify_ConnectionsMultiSelector.md create mode 100644 docs/api/Nodify_DecoratorsControl.md create mode 100644 docs/api/Nodify_Interactivity_EditorGestures_DirectionalNavigationGestures.md create mode 100644 docs/api/Nodify_Interactivity_EditorState_KeyboardNavigation.md create mode 100644 docs/api/Nodify_Interactivity_IKeyboardFocusTarget_TElement_.md create mode 100644 docs/api/Nodify_Interactivity_IKeyboardNavigationLayer.md create mode 100644 docs/api/Nodify_Interactivity_IKeyboardNavigationLayerGroup.md create mode 100644 docs/api/Nodify_Interactivity_InputGestureRefExtensions.md create mode 100644 docs/api/Nodify_Interactivity_KeyComboGesture.md create mode 100644 docs/api/Nodify_Interactivity_KeyboardNavigationLayerId.md create mode 100644 docs/api/Nodify_Interactivity_MinimapState_KeyboardNavigation.md create mode 100644 docs/api/Nodify_Interactivity_NodifyEditorGestures_KeyboardGestures.md diff --git a/docs/api/API-Reference.md b/docs/api/API-Reference.md index c44d29fc..7eff1aad 100644 --- a/docs/api/API-Reference.md +++ b/docs/api/API-Reference.md @@ -9,12 +9,15 @@ - [BoxValue Class](Nodify_BoxValue) - [CircuitConnection Class](Nodify_CircuitConnection) - [Connection Class](Nodify_Connection) +- [ConnectionContainer Class](Nodify_ConnectionContainer) - [ConnectionDirection Enum](Nodify_ConnectionDirection) - [ConnectionOffsetMode Enum](Nodify_ConnectionOffsetMode) +- [ConnectionsMultiSelector Class](Nodify_ConnectionsMultiSelector) - [Connector Class](Nodify_Connector) - [ConnectorPosition Enum](Nodify_ConnectorPosition) - [CuttingLine Class](Nodify_CuttingLine) - [DecoratorContainer Class](Nodify_DecoratorContainer) +- [DecoratorsControl Class](Nodify_DecoratorsControl) - [EditorCommands Class](Nodify_EditorCommands) - [GroupingMovementMode Enum](Nodify_GroupingMovementMode) - [GroupingNode Class](Nodify_GroupingNode) @@ -69,6 +72,7 @@ - [EditorGestures Class](Nodify_Interactivity_EditorGestures) - [ConnectionGestures Class](Nodify_Interactivity_EditorGestures_ConnectionGestures) - [ConnectorGestures Class](Nodify_Interactivity_EditorGestures_ConnectorGestures) + - [DirectionalNavigationGestures Class](Nodify_Interactivity_EditorGestures_DirectionalNavigationGestures) - [GroupingNodeGestures Class](Nodify_Interactivity_EditorGestures_GroupingNodeGestures) - [ItemContainerGestures Class](Nodify_Interactivity_EditorGestures_ItemContainerGestures) - [MinimapGestures Class](Nodify_Interactivity_EditorGestures_MinimapGestures) @@ -76,24 +80,33 @@ - [SelectionGestures Class](Nodify_Interactivity_EditorGestures_SelectionGestures) - [EditorState Class](Nodify_Interactivity_EditorState) - [Cutting Class](Nodify_Interactivity_EditorState_Cutting) + - [KeyboardNavigation Class](Nodify_Interactivity_EditorState_KeyboardNavigation) - [Panning Class](Nodify_Interactivity_EditorState_Panning) - [PanningWithMouseWheel Class](Nodify_Interactivity_EditorState_PanningWithMouseWheel) - [PushingItems Class](Nodify_Interactivity_EditorState_PushingItems) - [Selecting Class](Nodify_Interactivity_EditorState_Selecting) - [Zooming Class](Nodify_Interactivity_EditorState_Zooming) - [IInputHandler Interface](Nodify_Interactivity_IInputHandler) +- [IKeyboardFocusTarget\ Interface](Nodify_Interactivity_IKeyboardFocusTarget_TElement_) +- [IKeyboardNavigationLayer Interface](Nodify_Interactivity_IKeyboardNavigationLayer) +- [IKeyboardNavigationLayerGroup Interface](Nodify_Interactivity_IKeyboardNavigationLayerGroup) - [InputElementState\ Class](Nodify_Interactivity_InputElementState_TElement_) - [InputElementStateStack\ Class](Nodify_Interactivity_InputElementStateStack_TElement_) - [DragState\ Class](Nodify_Interactivity_InputElementStateStack_TElement__DragState_TElement_) - [IInputElementState\ Interface](Nodify_Interactivity_InputElementStateStack_TElement__IInputElementState_TElement_) - [InputElementState\ Class](Nodify_Interactivity_InputElementStateStack_TElement__InputElementState_TElement_) - [InputGestureRef Class](Nodify_Interactivity_InputGestureRef) +- [InputGestureRefExtensions Class](Nodify_Interactivity_InputGestureRefExtensions) - [InputProcessor Class](Nodify_Interactivity_InputProcessor) - [Shared\ Class](Nodify_Interactivity_InputProcessor_Shared_TElement_) - [InputProcessorExtensions Class](Nodify_Interactivity_InputProcessorExtensions) +- [KeyboardNavigationLayerId Class](Nodify_Interactivity_KeyboardNavigationLayerId) +- [KeyComboGesture Class](Nodify_Interactivity_KeyComboGesture) - [MinimapState Class](Nodify_Interactivity_MinimapState) + - [KeyboardNavigation Class](Nodify_Interactivity_MinimapState_KeyboardNavigation) - [Panning Class](Nodify_Interactivity_MinimapState_Panning) - [Zooming Class](Nodify_Interactivity_MinimapState_Zooming) - [MouseGesture Class](Nodify_Interactivity_MouseGesture) - [MultiGesture Class](Nodify_Interactivity_MultiGesture) - [Match Enum](Nodify_Interactivity_MultiGesture_Match) +- [NodifyEditorGestures.KeyboardGestures Class](Nodify_Interactivity_NodifyEditorGestures_KeyboardGestures) diff --git a/docs/api/Nodify_BaseConnection.md b/docs/api/Nodify_BaseConnection.md index 0c151bb8..a158719a 100644 --- a/docs/api/Nodify_BaseConnection.md +++ b/docs/api/Nodify_BaseConnection.md @@ -6,6 +6,8 @@ **Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [DispatcherObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Threading.DispatcherObject) → [DependencyObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyObject) → [Visual](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Media.Visual) → [UIElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.UIElement) → [FrameworkElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.FrameworkElement) → [Shape](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Shapes.Shape) → [BaseConnection](Nodify_BaseConnection) +**Implements:** [IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_) + **Derived:** [LineConnection](Nodify_LineConnection), [Connection](Nodify_Connection) **References:** [ArrowHeadEnds](Nodify_ArrowHeadEnds), [ArrowHeadShape](Nodify_ArrowHeadShape), [ConnectionDirection](Nodify_ConnectionDirection), [ConnectionEventArgs](Nodify_Events_ConnectionEventArgs), [ConnectionEventHandler](Nodify_Events_ConnectionEventHandler), [ConnectionOffsetMode](Nodify_ConnectionOffsetMode), [CuttingLine](Nodify_CuttingLine), [ConnectionState.Disconnect](Nodify_Interactivity_ConnectionState_Disconnect), [NodifyEditor](Nodify_NodifyEditor), [ConnectionState.Split](Nodify_Interactivity_ConnectionState_Split) @@ -13,7 +15,7 @@ Represents the base class for shapes that are drawn from a [BaseConnection.Source](Nodify_BaseConnection#source) point to a [BaseConnection.Target](Nodify_BaseConnection#target) point. ```csharp -public abstract class BaseConnection : Shape +public abstract class BaseConnection : Shape, IKeyboardFocusTarget ``` ## Constructors @@ -147,6 +149,42 @@ public ICommand DisconnectCommand { get; set; } [ICommand](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.ICommand) +### FocusVisualPadding + +The space between the focus visual and the connection geometry. + +```csharp +public double FocusVisualPadding { get; set; } +``` + +**Property Value** + +[Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double) + +### FocusVisualPen + +The pen used to render the focus visual. + +```csharp +public Pen FocusVisualPen { get; set; } +``` + +**Property Value** + +[Pen](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Media.Pen) + +### FocusVisualPenKey + +The key used to retrieve the [BaseConnection.FocusVisualPen](Nodify_BaseConnection#focusvisualpen) resource. + +```csharp +public static ResourceKey FocusVisualPenKey { get; set; } +``` + +**Property Value** + +[ResourceKey](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.ResourceKey) + ### FontFamily ```csharp diff --git a/docs/api/Nodify_ConnectionContainer.md b/docs/api/Nodify_ConnectionContainer.md new file mode 100644 index 00000000..842d2329 --- /dev/null +++ b/docs/api/Nodify_ConnectionContainer.md @@ -0,0 +1,165 @@ +# ConnectionContainer Class + +**Namespace:** Nodify + +**Assembly:** Nodify + +**Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [DispatcherObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Threading.DispatcherObject) → [DependencyObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyObject) → [Visual](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Media.Visual) → [UIElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.UIElement) → [FrameworkElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.FrameworkElement) → [ContentPresenter](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.ContentPresenter) → [ConnectionContainer](Nodify_ConnectionContainer) + +**Implements:** [IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_) + +**References:** [ConnectionsMultiSelector](Nodify_ConnectionsMultiSelector), [SelectionType](Nodify_SelectionType) + +```csharp +public class ConnectionContainer : ContentPresenter, IKeyboardFocusTarget +``` + +## Constructors + +### ConnectionContainer(ConnectionsMultiSelector) + +```csharp +public ConnectionContainer(ConnectionsMultiSelector selector); +``` + +**Parameters** + +`selector` [ConnectionsMultiSelector](Nodify_ConnectionsMultiSelector) + +## Properties + +### Bounds + +```csharp +public virtual Rect Bounds { get; set; } +``` + +**Property Value** + +[Rect](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Rect) + +### Connection + +```csharp +public FrameworkElement Connection { get; set; } +``` + +**Property Value** + +[FrameworkElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.FrameworkElement) + +### IsSelectable + +Gets or sets whether this [ConnectionContainer](Nodify_ConnectionContainer) can be selected. + +```csharp +public bool IsSelectable { get; set; } +``` + +**Property Value** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### IsSelected + +Gets or sets a value that indicates whether this [ConnectionContainer](Nodify_ConnectionContainer) is selected. + Can only be set if [ConnectionContainer.IsSelectable](Nodify_ConnectionContainer#isselectable) is true. + +```csharp +public bool IsSelected { get; set; } +``` + +**Property Value** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### Selector + +```csharp +public ConnectionsMultiSelector Selector { get; set; } +``` + +**Property Value** + +[ConnectionsMultiSelector](Nodify_ConnectionsMultiSelector) + +## Methods + +### OnIsKeyboardFocusedChanged(DependencyPropertyChangedEventArgs) + +```csharp +protected override void OnIsKeyboardFocusedChanged(DependencyPropertyChangedEventArgs e); +``` + +**Parameters** + +`e` [DependencyPropertyChangedEventArgs](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyPropertyChangedEventArgs) + +### OnMouseDown(MouseButtonEventArgs) + +```csharp +protected override void OnMouseDown(MouseButtonEventArgs e); +``` + +**Parameters** + +`e` [MouseButtonEventArgs](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.MouseButtonEventArgs) + +### OnMouseUp(MouseButtonEventArgs) + +```csharp +protected override void OnMouseUp(MouseButtonEventArgs e); +``` + +**Parameters** + +`e` [MouseButtonEventArgs](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.MouseButtonEventArgs) + +### OnVisualParentChanged(DependencyObject) + +```csharp +protected override void OnVisualParentChanged(DependencyObject oldParent); +``` + +**Parameters** + +`oldParent` [DependencyObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyObject) + +### Select(SelectionType) + +Modifies the selection state of the current item based on the specified selection type. + +```csharp +public void Select(SelectionType type); +``` + +**Parameters** + +`type` [SelectionType](Nodify_SelectionType): The type of selection to perform. + +## Events + +### Selected + +Occurs when this [ConnectionContainer](Nodify_ConnectionContainer) is selected. + +```csharp +public event RoutedEventHandler Selected; +``` + +**Event Type** + +[RoutedEventHandler](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.RoutedEventHandler) + +### Unselected + +Occurs when this [ConnectionContainer](Nodify_ConnectionContainer) is unselected. + +```csharp +public event RoutedEventHandler Unselected; +``` + +**Event Type** + +[RoutedEventHandler](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.RoutedEventHandler) + diff --git a/docs/api/Nodify_ConnectionsMultiSelector.md b/docs/api/Nodify_ConnectionsMultiSelector.md new file mode 100644 index 00000000..dfeb7a60 --- /dev/null +++ b/docs/api/Nodify_ConnectionsMultiSelector.md @@ -0,0 +1,184 @@ +# ConnectionsMultiSelector Class + +**Namespace:** Nodify + +**Assembly:** Nodify + +**Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [DispatcherObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Threading.DispatcherObject) → [DependencyObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyObject) → [Visual](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Media.Visual) → [UIElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.UIElement) → [FrameworkElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.FrameworkElement) → [Control](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.Control) → [ItemsControl](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.ItemsControl) → [Selector](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.Primitives.Selector) → [MultiSelector](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.Primitives.MultiSelector) → [ConnectionsMultiSelector](Nodify_ConnectionsMultiSelector) + +**Implements:** [IKeyboardNavigationLayer](Nodify_Interactivity_IKeyboardNavigationLayer) + +**References:** [ConnectionContainer](Nodify_ConnectionContainer), [IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_), [IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_), [KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId), [NodifyEditor](Nodify_NodifyEditor) + +```csharp +public class ConnectionsMultiSelector : MultiSelector, IKeyboardNavigationLayer +``` + +## Constructors + +### ConnectionsMultiSelector() + +```csharp +public ConnectionsMultiSelector(); +``` + +## Properties + +### CanSelectMultipleItems + +Gets or sets whether multiple connections can be selected. + +```csharp +public bool CanSelectMultipleItems { get; set; } +``` + +**Property Value** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### Editor + +Gets the [NodifyEditor](Nodify_NodifyEditor) that owns this [ConnectionsMultiSelector](Nodify_ConnectionsMultiSelector). + +```csharp +public NodifyEditor Editor { get; set; } +``` + +**Property Value** + +[NodifyEditor](Nodify_NodifyEditor) + +### Id + +```csharp +public virtual KeyboardNavigationLayerId Id { get; set; } +``` + +**Property Value** + +[KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId) + +### LastFocusedElement + +```csharp +public virtual IKeyboardFocusTarget LastFocusedElement { get; set; } +``` + +**Property Value** + +[IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_) + +### SelectedItems + +Gets or sets the selected connections in the [NodifyEditor](Nodify_NodifyEditor). + +```csharp +public IList SelectedItems { get; set; } +``` + +**Property Value** + +[IList](https://docs.microsoft.com/en-us/dotnet/api/System.Collections.IList) + +## Methods + +### FindNextFocusTarget(ConnectionContainer, TraversalRequest) + +```csharp +protected virtual ConnectionContainer FindNextFocusTarget(ConnectionContainer currentContainer, TraversalRequest request); +``` + +**Parameters** + +`currentContainer` [ConnectionContainer](Nodify_ConnectionContainer) + +`request` [TraversalRequest](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.TraversalRequest) + +**Returns** + +[ConnectionContainer](Nodify_ConnectionContainer) + +### GetContainerForItemOverride() + +```csharp +protected override DependencyObject GetContainerForItemOverride(); +``` + +**Returns** + +[DependencyObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyObject) + +### IsItemItsOwnContainerOverride(Object) + +```csharp +protected override bool IsItemItsOwnContainerOverride(object item); +``` + +**Parameters** + +`item` [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### OnApplyTemplate() + +```csharp +public override void OnApplyTemplate(); +``` + +### OnElementFocused(IKeyboardFocusTarget\) + +```csharp +protected virtual void OnElementFocused(IKeyboardFocusTarget target); +``` + +**Parameters** + +`target` [IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_) + +### OnSelectionChanged(SelectionChangedEventArgs) + +```csharp +protected override void OnSelectionChanged(SelectionChangedEventArgs e); +``` + +**Parameters** + +`e` [SelectionChangedEventArgs](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.SelectionChangedEventArgs) + +### Select(ConnectionContainer) + +```csharp +public void Select(ConnectionContainer container); +``` + +**Parameters** + +`container` [ConnectionContainer](Nodify_ConnectionContainer) + +### TryMoveFocus(TraversalRequest) + +```csharp +public virtual bool TryMoveFocus(TraversalRequest request); +``` + +**Parameters** + +`request` [TraversalRequest](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.TraversalRequest) + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### TryRestoreFocus() + +```csharp +public virtual bool TryRestoreFocus(); +``` + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + diff --git a/docs/api/Nodify_DecoratorContainer.md b/docs/api/Nodify_DecoratorContainer.md index 8abc6121..d3c60b15 100644 --- a/docs/api/Nodify_DecoratorContainer.md +++ b/docs/api/Nodify_DecoratorContainer.md @@ -6,18 +6,28 @@ **Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [DispatcherObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Threading.DispatcherObject) → [DependencyObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyObject) → [Visual](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Media.Visual) → [UIElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.UIElement) → [FrameworkElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.FrameworkElement) → [Control](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.Control) → [ContentControl](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.ContentControl) → [DecoratorContainer](Nodify_DecoratorContainer) -**Implements:** [INodifyCanvasItem](Nodify_INodifyCanvasItem) +**Implements:** [INodifyCanvasItem](Nodify_INodifyCanvasItem), [IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_) -**References:** [NodifyEditor](Nodify_NodifyEditor) +**References:** [DecoratorsControl](Nodify_DecoratorsControl), [NodifyEditor](Nodify_NodifyEditor) The container for all the items generated from the [NodifyEditor.Decorators](Nodify_NodifyEditor#decorators) collection. ```csharp -public class DecoratorContainer : ContentControl, INodifyCanvasItem +public class DecoratorContainer : ContentControl, INodifyCanvasItem, IKeyboardFocusTarget ``` ## Constructors +### DecoratorContainer(DecoratorsControl) + +```csharp +public DecoratorContainer(DecoratorsControl parent); +``` + +**Parameters** + +`parent` [DecoratorsControl](Nodify_DecoratorsControl) + ### DecoratorContainer() ```csharp @@ -38,6 +48,16 @@ public Size ActualSize { get; set; } [Size](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Size) +### Bounds + +```csharp +public virtual Rect Bounds { get; set; } +``` + +**Property Value** + +[Rect](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Rect) + ### Location Gets or sets the location of this [DecoratorContainer](Nodify_DecoratorContainer) inside the NodifyEditor.DecoratorsHost. @@ -50,6 +70,16 @@ public virtual Point Location { get; set; } [Point](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Point) +### Owner + +```csharp +public DecoratorsControl Owner { get; set; } +``` + +**Property Value** + +[DecoratorsControl](Nodify_DecoratorsControl) + ## Methods ### OnLocationChanged() @@ -70,6 +100,16 @@ protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo); `sizeInfo` [SizeChangedInfo](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.SizeChangedInfo) +### OnVisualParentChanged(DependencyObject) + +```csharp +protected override void OnVisualParentChanged(DependencyObject oldParent); +``` + +**Parameters** + +`oldParent` [DependencyObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyObject) + ## Events ### LocationChanged diff --git a/docs/api/Nodify_DecoratorsControl.md b/docs/api/Nodify_DecoratorsControl.md new file mode 100644 index 00000000..035464a1 --- /dev/null +++ b/docs/api/Nodify_DecoratorsControl.md @@ -0,0 +1,142 @@ +# DecoratorsControl Class + +**Namespace:** Nodify + +**Assembly:** Nodify + +**Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [DispatcherObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Threading.DispatcherObject) → [DependencyObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyObject) → [Visual](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Media.Visual) → [UIElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.UIElement) → [FrameworkElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.FrameworkElement) → [Control](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.Control) → [ItemsControl](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.ItemsControl) → [DecoratorsControl](Nodify_DecoratorsControl) + +**Implements:** [IKeyboardNavigationLayer](Nodify_Interactivity_IKeyboardNavigationLayer) + +**References:** [DecoratorContainer](Nodify_DecoratorContainer), [IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_), [IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_), [KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId), [NodifyEditor](Nodify_NodifyEditor) + +An [ItemsControl](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.ItemsControl) that works with [DecoratorContainer](Nodify_DecoratorContainer)s. + +```csharp +public class DecoratorsControl : ItemsControl, IKeyboardNavigationLayer +``` + +## Constructors + +### DecoratorsControl() + +```csharp +public DecoratorsControl(); +``` + +## Properties + +### Editor + +Gets the [NodifyEditor](Nodify_NodifyEditor) that owns this [DecoratorsControl](Nodify_DecoratorsControl). + +```csharp +public NodifyEditor Editor { get; set; } +``` + +**Property Value** + +[NodifyEditor](Nodify_NodifyEditor) + +### Id + +```csharp +public virtual KeyboardNavigationLayerId Id { get; set; } +``` + +**Property Value** + +[KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId) + +### LastFocusedElement + +```csharp +public virtual IKeyboardFocusTarget LastFocusedElement { get; set; } +``` + +**Property Value** + +[IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_) + +## Methods + +### FindNextFocusTarget(DecoratorContainer, TraversalRequest) + +```csharp +protected virtual DecoratorContainer FindNextFocusTarget(DecoratorContainer currentContainer, TraversalRequest request); +``` + +**Parameters** + +`currentContainer` [DecoratorContainer](Nodify_DecoratorContainer) + +`request` [TraversalRequest](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.TraversalRequest) + +**Returns** + +[DecoratorContainer](Nodify_DecoratorContainer) + +### GetContainerForItemOverride() + +```csharp +protected override DependencyObject GetContainerForItemOverride(); +``` + +**Returns** + +[DependencyObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyObject) + +### IsItemItsOwnContainerOverride(Object) + +```csharp +protected override bool IsItemItsOwnContainerOverride(object item); +``` + +**Parameters** + +`item` [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### OnApplyTemplate() + +```csharp +public override void OnApplyTemplate(); +``` + +### OnElementFocused(IKeyboardFocusTarget\) + +```csharp +protected virtual void OnElementFocused(IKeyboardFocusTarget target); +``` + +**Parameters** + +`target` [IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_) + +### TryMoveFocus(TraversalRequest) + +```csharp +public virtual bool TryMoveFocus(TraversalRequest request); +``` + +**Parameters** + +`request` [TraversalRequest](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.TraversalRequest) + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### TryRestoreFocus() + +```csharp +public virtual bool TryRestoreFocus(); +``` + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + diff --git a/docs/api/Nodify_GroupingNode.md b/docs/api/Nodify_GroupingNode.md index 4ec08164..b3afe7d6 100644 --- a/docs/api/Nodify_GroupingNode.md +++ b/docs/api/Nodify_GroupingNode.md @@ -210,6 +210,16 @@ public ICommand ResizeStartedCommand { get; set; } public override void OnApplyTemplate(); ``` +### ToggleContentSelection() + +Toggles the selection of nodes inside this group. + If any contained nodes are selected, all will be unselected. + If none are selected, all will be selected. + +```csharp +public void ToggleContentSelection(); +``` + ## Events ### ResizeCompleted diff --git a/docs/api/Nodify_Interactivity_ConnectorState_Disconnect.md b/docs/api/Nodify_Interactivity_ConnectorState_Disconnect.md index 11898348..f25fb60b 100644 --- a/docs/api/Nodify_Interactivity_ConnectorState_Disconnect.md +++ b/docs/api/Nodify_Interactivity_ConnectorState_Disconnect.md @@ -26,6 +26,16 @@ public Disconnect(Connector connector); ## Methods +### OnKeyDown(KeyEventArgs) + +```csharp +protected override void OnKeyDown(KeyEventArgs e); +``` + +**Parameters** + +`e` [KeyEventArgs](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.KeyEventArgs) + ### OnMouseDown(MouseButtonEventArgs) ```csharp diff --git a/docs/api/Nodify_Interactivity_EditorGestures.md b/docs/api/Nodify_Interactivity_EditorGestures.md index 3fc9c110..16ff6b02 100644 --- a/docs/api/Nodify_Interactivity_EditorGestures.md +++ b/docs/api/Nodify_Interactivity_EditorGestures.md @@ -122,3 +122,11 @@ public void Apply(EditorGestures gestures); `gestures` [EditorGestures](Nodify_Interactivity_EditorGestures): The gestures to copy. +### Unbind() + +Unbinds all the gestures used by the editor and its controls. + +```csharp +public void Unbind(); +``` + diff --git a/docs/api/Nodify_Interactivity_EditorGestures_ConnectionGestures.md b/docs/api/Nodify_Interactivity_EditorGestures_ConnectionGestures.md index 77e432d1..67ee3c48 100644 --- a/docs/api/Nodify_Interactivity_EditorGestures_ConnectionGestures.md +++ b/docs/api/Nodify_Interactivity_EditorGestures_ConnectionGestures.md @@ -64,3 +64,9 @@ public void Apply(EditorGestures.ConnectionGestures gestures); `gestures` [EditorGestures.ConnectionGestures](Nodify_Interactivity_EditorGestures_ConnectionGestures) +### Unbind() + +```csharp +public void Unbind(); +``` + diff --git a/docs/api/Nodify_Interactivity_EditorGestures_ConnectorGestures.md b/docs/api/Nodify_Interactivity_EditorGestures_ConnectorGestures.md index 69ba11cd..ec449dab 100644 --- a/docs/api/Nodify_Interactivity_EditorGestures_ConnectorGestures.md +++ b/docs/api/Nodify_Interactivity_EditorGestures_ConnectorGestures.md @@ -64,3 +64,9 @@ public void Apply(EditorGestures.ConnectorGestures gestures); `gestures` [EditorGestures.ConnectorGestures](Nodify_Interactivity_EditorGestures_ConnectorGestures) +### Unbind() + +```csharp +public void Unbind(); +``` + diff --git a/docs/api/Nodify_Interactivity_EditorGestures_DirectionalNavigationGestures.md b/docs/api/Nodify_Interactivity_EditorGestures_DirectionalNavigationGestures.md new file mode 100644 index 00000000..15b79a6a --- /dev/null +++ b/docs/api/Nodify_Interactivity_EditorGestures_DirectionalNavigationGestures.md @@ -0,0 +1,100 @@ +# EditorGestures.DirectionalNavigationGestures Class + +**Namespace:** Nodify.Interactivity + +**Assembly:** Nodify + +**Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [EditorGestures.DirectionalNavigationGestures](Nodify_Interactivity_EditorGestures_DirectionalNavigationGestures) + +**References:** [InputGestureRef](Nodify_Interactivity_InputGestureRef), [NodifyEditorGestures.KeyboardGestures](Nodify_Interactivity_NodifyEditorGestures_KeyboardGestures), [EditorGestures.MinimapGestures](Nodify_Interactivity_EditorGestures_MinimapGestures) + +```csharp +public class DirectionalNavigationGestures +``` + +## Constructors + +### EditorGestures.DirectionalNavigationGestures(ModifierKeys) + +```csharp +public DirectionalNavigationGestures(ModifierKeys modifierKeys = 0); +``` + +**Parameters** + +`modifierKeys` [ModifierKeys](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.ModifierKeys) + +### EditorGestures.DirectionalNavigationGestures(Key, ModifierKeys, Boolean) + +```csharp +public DirectionalNavigationGestures(Key triggerKey, ModifierKeys modifierKeys = 0, bool repeated = false); +``` + +**Parameters** + +`triggerKey` [Key](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.Key) + +`modifierKeys` [ModifierKeys](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.ModifierKeys) + +`repeated` [Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +## Properties + +### Down + +```csharp +public InputGestureRef Down { get; set; } +``` + +**Property Value** + +[InputGestureRef](Nodify_Interactivity_InputGestureRef) + +### Left + +```csharp +public InputGestureRef Left { get; set; } +``` + +**Property Value** + +[InputGestureRef](Nodify_Interactivity_InputGestureRef) + +### Right + +```csharp +public InputGestureRef Right { get; set; } +``` + +**Property Value** + +[InputGestureRef](Nodify_Interactivity_InputGestureRef) + +### Up + +```csharp +public InputGestureRef Up { get; set; } +``` + +**Property Value** + +[InputGestureRef](Nodify_Interactivity_InputGestureRef) + +## Methods + +### Apply(EditorGestures.DirectionalNavigationGestures) + +```csharp +public void Apply(EditorGestures.DirectionalNavigationGestures gestures); +``` + +**Parameters** + +`gestures` [EditorGestures.DirectionalNavigationGestures](Nodify_Interactivity_EditorGestures_DirectionalNavigationGestures) + +### Unbind() + +```csharp +public void Unbind(); +``` + diff --git a/docs/api/Nodify_Interactivity_EditorGestures_GroupingNodeGestures.md b/docs/api/Nodify_Interactivity_EditorGestures_GroupingNodeGestures.md index cf86dd01..1d01018b 100644 --- a/docs/api/Nodify_Interactivity_EditorGestures_GroupingNodeGestures.md +++ b/docs/api/Nodify_Interactivity_EditorGestures_GroupingNodeGestures.md @@ -6,7 +6,7 @@ **Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [EditorGestures.GroupingNodeGestures](Nodify_Interactivity_EditorGestures_GroupingNodeGestures) -**References:** [EditorGestures](Nodify_Interactivity_EditorGestures) +**References:** [EditorGestures](Nodify_Interactivity_EditorGestures), [InputGestureRef](Nodify_Interactivity_InputGestureRef) ```csharp public class GroupingNodeGestures @@ -32,6 +32,16 @@ public ModifierKeys SwitchMovementMode { get; set; } [ModifierKeys](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.ModifierKeys) +### ToggleContentSelection + +```csharp +public InputGestureRef ToggleContentSelection { get; set; } +``` + +**Property Value** + +[InputGestureRef](Nodify_Interactivity_InputGestureRef) + ## Methods ### Apply(EditorGestures.GroupingNodeGestures) @@ -44,3 +54,9 @@ public void Apply(EditorGestures.GroupingNodeGestures gestures); `gestures` [EditorGestures.GroupingNodeGestures](Nodify_Interactivity_EditorGestures_GroupingNodeGestures) +### Unbind() + +```csharp +public void Unbind(); +``` + diff --git a/docs/api/Nodify_Interactivity_EditorGestures_ItemContainerGestures.md b/docs/api/Nodify_Interactivity_EditorGestures_ItemContainerGestures.md index ed0b9d30..3ffefc5b 100644 --- a/docs/api/Nodify_Interactivity_EditorGestures_ItemContainerGestures.md +++ b/docs/api/Nodify_Interactivity_EditorGestures_ItemContainerGestures.md @@ -64,3 +64,9 @@ public void Apply(EditorGestures.ItemContainerGestures gestures); `gestures` [EditorGestures.ItemContainerGestures](Nodify_Interactivity_EditorGestures_ItemContainerGestures) +### Unbind() + +```csharp +public void Unbind(); +``` + diff --git a/docs/api/Nodify_Interactivity_EditorGestures_MinimapGestures.md b/docs/api/Nodify_Interactivity_EditorGestures_MinimapGestures.md index 3bb94a60..c6a81995 100644 --- a/docs/api/Nodify_Interactivity_EditorGestures_MinimapGestures.md +++ b/docs/api/Nodify_Interactivity_EditorGestures_MinimapGestures.md @@ -6,7 +6,7 @@ **Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [EditorGestures.MinimapGestures](Nodify_Interactivity_EditorGestures_MinimapGestures) -**References:** [EditorGestures](Nodify_Interactivity_EditorGestures), [InputGestureRef](Nodify_Interactivity_InputGestureRef) +**References:** [EditorGestures.DirectionalNavigationGestures](Nodify_Interactivity_EditorGestures_DirectionalNavigationGestures), [EditorGestures](Nodify_Interactivity_EditorGestures), [InputGestureRef](Nodify_Interactivity_InputGestureRef) ```csharp public class MinimapGestures @@ -42,6 +42,36 @@ public InputGestureRef DragViewport { get; set; } [InputGestureRef](Nodify_Interactivity_InputGestureRef) +### Pan + +```csharp +public DirectionalNavigationGestures Pan { get; set; } +``` + +**Property Value** + +[EditorGestures.DirectionalNavigationGestures](Nodify_Interactivity_EditorGestures_DirectionalNavigationGestures) + +### ResetViewport + +```csharp +public InputGestureRef ResetViewport { get; set; } +``` + +**Property Value** + +[InputGestureRef](Nodify_Interactivity_InputGestureRef) + +### ZoomIn + +```csharp +public InputGestureRef ZoomIn { get; set; } +``` + +**Property Value** + +[InputGestureRef](Nodify_Interactivity_InputGestureRef) + ### ZoomModifierKey ```csharp @@ -52,6 +82,16 @@ public ModifierKeys ZoomModifierKey { get; set; } [ModifierKeys](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.ModifierKeys) +### ZoomOut + +```csharp +public InputGestureRef ZoomOut { get; set; } +``` + +**Property Value** + +[InputGestureRef](Nodify_Interactivity_InputGestureRef) + ## Methods ### Apply(EditorGestures.MinimapGestures) @@ -64,3 +104,9 @@ public void Apply(EditorGestures.MinimapGestures gestures); `gestures` [EditorGestures.MinimapGestures](Nodify_Interactivity_EditorGestures_MinimapGestures) +### Unbind() + +```csharp +public void Unbind(); +``` + diff --git a/docs/api/Nodify_Interactivity_EditorGestures_NodifyEditorGestures.md b/docs/api/Nodify_Interactivity_EditorGestures_NodifyEditorGestures.md index 810ad6cc..6b1c166b 100644 --- a/docs/api/Nodify_Interactivity_EditorGestures_NodifyEditorGestures.md +++ b/docs/api/Nodify_Interactivity_EditorGestures_NodifyEditorGestures.md @@ -6,7 +6,7 @@ **Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [EditorGestures.NodifyEditorGestures](Nodify_Interactivity_EditorGestures_NodifyEditorGestures) -**References:** [EditorGestures](Nodify_Interactivity_EditorGestures), [InputGestureRef](Nodify_Interactivity_InputGestureRef), [EditorGestures.SelectionGestures](Nodify_Interactivity_EditorGestures_SelectionGestures) +**References:** [EditorGestures](Nodify_Interactivity_EditorGestures), [InputGestureRef](Nodify_Interactivity_InputGestureRef), [NodifyEditorGestures.KeyboardGestures](Nodify_Interactivity_NodifyEditorGestures_KeyboardGestures), [EditorGestures.SelectionGestures](Nodify_Interactivity_EditorGestures_SelectionGestures) ```csharp public class NodifyEditorGestures @@ -52,6 +52,16 @@ public InputGestureRef FitToScreen { get; set; } [InputGestureRef](Nodify_Interactivity_InputGestureRef) +### Keyboard + +```csharp +public KeyboardGestures Keyboard { get; set; } +``` + +**Property Value** + +[NodifyEditorGestures.KeyboardGestures](Nodify_Interactivity_NodifyEditorGestures_KeyboardGestures) + ### Pan ```csharp @@ -102,10 +112,20 @@ public InputGestureRef PushItems { get; set; } [InputGestureRef](Nodify_Interactivity_InputGestureRef) -### ResetViewportLocation +### ResetViewport ```csharp -public InputGestureRef ResetViewportLocation { get; set; } +public InputGestureRef ResetViewport { get; set; } +``` + +**Property Value** + +[InputGestureRef](Nodify_Interactivity_InputGestureRef) + +### SelectAll + +```csharp +public InputGestureRef SelectAll { get; set; } ``` **Property Value** @@ -164,3 +184,9 @@ public void Apply(EditorGestures.NodifyEditorGestures gestures); `gestures` [EditorGestures.NodifyEditorGestures](Nodify_Interactivity_EditorGestures_NodifyEditorGestures) +### Unbind() + +```csharp +public void Unbind(); +``` + diff --git a/docs/api/Nodify_Interactivity_EditorState_KeyboardNavigation.md b/docs/api/Nodify_Interactivity_EditorState_KeyboardNavigation.md new file mode 100644 index 00000000..ab608828 --- /dev/null +++ b/docs/api/Nodify_Interactivity_EditorState_KeyboardNavigation.md @@ -0,0 +1,48 @@ +# EditorState.KeyboardNavigation Class + +**Namespace:** Nodify.Interactivity + +**Assembly:** Nodify + +**Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [InputElementState\](Nodify_Interactivity_InputElementState_TElement_) → [EditorState.KeyboardNavigation](Nodify_Interactivity_EditorState_KeyboardNavigation) + +**References:** [NodifyEditor](Nodify_NodifyEditor) + +```csharp +public class KeyboardNavigation : InputElementState +``` + +## Constructors + +### EditorState.KeyboardNavigation(NodifyEditor) + +```csharp +public KeyboardNavigation(NodifyEditor element); +``` + +**Parameters** + +`element` [NodifyEditor](Nodify_NodifyEditor) + +## Methods + +### OnKeyDown(KeyEventArgs) + +```csharp +protected override void OnKeyDown(KeyEventArgs e); +``` + +**Parameters** + +`e` [KeyEventArgs](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.KeyEventArgs) + +### OnKeyUp(KeyEventArgs) + +```csharp +protected override void OnKeyUp(KeyEventArgs e); +``` + +**Parameters** + +`e` [KeyEventArgs](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.KeyEventArgs) + diff --git a/docs/api/Nodify_Interactivity_IInputHandler.md b/docs/api/Nodify_Interactivity_IInputHandler.md index b2ad805f..1f53c861 100644 --- a/docs/api/Nodify_Interactivity_IInputHandler.md +++ b/docs/api/Nodify_Interactivity_IInputHandler.md @@ -16,6 +16,18 @@ public interface IInputHandler ## Properties +### ProcessHandledEvents + +Gets or sets a value indicating whether events that have been handled should be processed too. + +```csharp +public virtual bool ProcessHandledEvents { get; set; } +``` + +**Property Value** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + ### RequiresInputCapture Gets a value indicating whether the handler requires input capture to remain active. diff --git a/docs/api/Nodify_Interactivity_IKeyboardFocusTarget_TElement_.md b/docs/api/Nodify_Interactivity_IKeyboardFocusTarget_TElement_.md new file mode 100644 index 00000000..b21e2eef --- /dev/null +++ b/docs/api/Nodify_Interactivity_IKeyboardFocusTarget_TElement_.md @@ -0,0 +1,32 @@ +# IKeyboardFocusTarget\ Interface + +**Namespace:** Nodify.Interactivity + +**Assembly:** Nodify + +```csharp +public interface IKeyboardFocusTarget +``` + +## Properties + +### Bounds + +```csharp +public virtual Rect Bounds { get; set; } +``` + +**Property Value** + +[Rect](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Rect) + +### Element + +```csharp +public virtual TElement Element { get; set; } +``` + +**Property Value** + +[TElement](Nodify_Interactivity_IKeyboardFocusTarget_TElement__TElement) + diff --git a/docs/api/Nodify_Interactivity_IKeyboardNavigationLayer.md b/docs/api/Nodify_Interactivity_IKeyboardNavigationLayer.md new file mode 100644 index 00000000..5910cfc5 --- /dev/null +++ b/docs/api/Nodify_Interactivity_IKeyboardNavigationLayer.md @@ -0,0 +1,88 @@ +# IKeyboardNavigationLayer Interface + +**Namespace:** Nodify.Interactivity + +**Assembly:** Nodify + +**Derived:** [ConnectionsMultiSelector](Nodify_ConnectionsMultiSelector), [NodifyEditor](Nodify_NodifyEditor), [DecoratorsControl](Nodify_DecoratorsControl) + +**References:** [IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_), [IKeyboardNavigationLayerGroup](Nodify_Interactivity_IKeyboardNavigationLayerGroup), [KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId), [NodifyEditor](Nodify_NodifyEditor) + +Represents a layer of keyboard navigation that can handle focus movement and restoration. + +```csharp +public interface IKeyboardNavigationLayer +``` + +## Properties + +### Id + +Gets the unique identifier for this keyboard navigation layer. + +```csharp +public virtual KeyboardNavigationLayerId Id { get; set; } +``` + +**Property Value** + +[KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId) + +### LastFocusedElement + +Gets the last focused element within this layer, if any. + +```csharp +public virtual IKeyboardFocusTarget LastFocusedElement { get; set; } +``` + +**Property Value** + +[IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_) + +## Methods + +### OnActivated() + +Called when the layer is activated, allowing for any necessary setup or focus management. + +```csharp +public virtual void OnActivated(); +``` + +### OnDeactivated() + +Called when the layer is deactivated, allowing for any necessary cleanup or focus management. + +```csharp +public virtual void OnDeactivated(); +``` + +### TryMoveFocus(TraversalRequest) + +Attempts to move focus within this layer based on the provided traversal request. + +```csharp +public virtual bool TryMoveFocus(TraversalRequest request); +``` + +**Parameters** + +`request` [TraversalRequest](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.TraversalRequest): The traversal request. + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean): Returns true if the focus was moved, false otherwise. + +### TryRestoreFocus() + +Attempts to restore focus to the last focused element within this layer. + +```csharp +public virtual bool TryRestoreFocus(); +``` + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean): Returns true if the focus was restored, false otherwise. + diff --git a/docs/api/Nodify_Interactivity_IKeyboardNavigationLayerGroup.md b/docs/api/Nodify_Interactivity_IKeyboardNavigationLayerGroup.md new file mode 100644 index 00000000..76124fff --- /dev/null +++ b/docs/api/Nodify_Interactivity_IKeyboardNavigationLayerGroup.md @@ -0,0 +1,120 @@ +# IKeyboardNavigationLayerGroup Interface + +**Namespace:** Nodify.Interactivity + +**Assembly:** Nodify + +**Implements:** [IReadOnlyCollection\](https://docs.microsoft.com/en-us/dotnet/api/System.Collections.Generic.IReadOnlyCollection-1) + +**Derived:** [NodifyEditor](Nodify_NodifyEditor) + +**References:** [IKeyboardNavigationLayer](Nodify_Interactivity_IKeyboardNavigationLayer), [KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId) + +Represents a group of keyboard navigation layers that can be activated and navigated through. + +```csharp +public interface IKeyboardNavigationLayerGroup : IReadOnlyCollection +``` + +## Properties + +### ActiveNavigationLayer + +The current active keyboard navigation layer in the group, if any. + +```csharp +public virtual IKeyboardNavigationLayer ActiveNavigationLayer { get; set; } +``` + +**Property Value** + +[IKeyboardNavigationLayer](Nodify_Interactivity_IKeyboardNavigationLayer) + +## Methods + +### ActivateNavigationLayer(KeyboardNavigationLayerId) + +Activates the specified keyboard navigation layer, making it the active layer for focus management. + +```csharp +public virtual bool ActivateNavigationLayer(KeyboardNavigationLayerId layerId); +``` + +**Parameters** + +`layerId` [KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId): The navigation layer id to activate. + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean): Returns true if the navigation layer was activated, false otherwise. + +### ActivateNextNavigationLayer() + +Activates the next keyboard navigation layer in the group, allowing focus to be restored to the last focused element in that layer. + +```csharp +public virtual bool ActivateNextNavigationLayer(); +``` + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean): Returns true if the navigation layer was activated, false otherwise. + +### ActivatePreviousNavigationLayer() + +Activates the previous keyboard navigation layer in the group, allowing focus to be restored to the last focused element in that layer. + +```csharp +public virtual bool ActivatePreviousNavigationLayer(); +``` + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean): Returns true if the navigation layer was activated, false otherwise. + +### RegisterNavigationLayer(IKeyboardNavigationLayer) + +Registers a new keyboard navigation layer to the group, allowing it to handle focus movement and restoration. + +```csharp +public virtual bool RegisterNavigationLayer(IKeyboardNavigationLayer layer); +``` + +**Parameters** + +`layer` [IKeyboardNavigationLayer](Nodify_Interactivity_IKeyboardNavigationLayer): The navigation layer. + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### RemoveNavigationLayer(KeyboardNavigationLayerId) + +Removes the specified keyboard navigation layer from the group. + +```csharp +public virtual bool RemoveNavigationLayer(KeyboardNavigationLayerId layerId); +``` + +**Parameters** + +`layerId` [KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId): The navigation layer id. + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean): Returns true if the layer was removed, false otherwise. + +## Events + +### ActiveNavigationLayerChanged + +Event that is raised when the active keyboard navigation layer changes. + +```csharp +public virtual event Action ActiveNavigationLayerChanged; +``` + +**Event Type** + +[Action\](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1) + diff --git a/docs/api/Nodify_Interactivity_InputElementStateStack_TElement_.md b/docs/api/Nodify_Interactivity_InputElementStateStack_TElement_.md index 73491157..dd8bb594 100644 --- a/docs/api/Nodify_Interactivity_InputElementStateStack_TElement_.md +++ b/docs/api/Nodify_Interactivity_InputElementStateStack_TElement_.md @@ -38,6 +38,16 @@ protected TElement Element { get; set; } [TElement](Nodify_Interactivity_InputElementStateStack_TElement__TElement) +### ProcessHandledEvents + +```csharp +public virtual bool ProcessHandledEvents { get; set; } +``` + +**Property Value** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + ### RequiresInputCapture ```csharp diff --git a/docs/api/Nodify_Interactivity_InputElementState_TElement_.md b/docs/api/Nodify_Interactivity_InputElementState_TElement_.md index d11dbd38..2ab11ed5 100644 --- a/docs/api/Nodify_Interactivity_InputElementState_TElement_.md +++ b/docs/api/Nodify_Interactivity_InputElementState_TElement_.md @@ -36,6 +36,16 @@ protected TElement Element { get; set; } [TElement](Nodify_Interactivity_InputElementState_TElement__TElement) +### ProcessHandledEvents + +```csharp +public virtual bool ProcessHandledEvents { get; protected set; } +``` + +**Property Value** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + ### RequiresInputCapture ```csharp diff --git a/docs/api/Nodify_Interactivity_InputGestureRef.md b/docs/api/Nodify_Interactivity_InputGestureRef.md index 3e3c914e..913e5a68 100644 --- a/docs/api/Nodify_Interactivity_InputGestureRef.md +++ b/docs/api/Nodify_Interactivity_InputGestureRef.md @@ -6,7 +6,7 @@ **Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [InputGesture](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.InputGesture) → [InputGestureRef](Nodify_Interactivity_InputGestureRef) -**References:** [EditorGestures.ConnectionGestures](Nodify_Interactivity_EditorGestures_ConnectionGestures), [EditorGestures.ConnectorGestures](Nodify_Interactivity_EditorGestures_ConnectorGestures), [EditorCommands](Nodify_EditorCommands), [EditorGestures.ItemContainerGestures](Nodify_Interactivity_EditorGestures_ItemContainerGestures), [EditorGestures.MinimapGestures](Nodify_Interactivity_EditorGestures_MinimapGestures), [EditorGestures.NodifyEditorGestures](Nodify_Interactivity_EditorGestures_NodifyEditorGestures), [EditorGestures.SelectionGestures](Nodify_Interactivity_EditorGestures_SelectionGestures) +**References:** [EditorGestures.ConnectionGestures](Nodify_Interactivity_EditorGestures_ConnectionGestures), [EditorGestures.ConnectorGestures](Nodify_Interactivity_EditorGestures_ConnectorGestures), [EditorGestures.DirectionalNavigationGestures](Nodify_Interactivity_EditorGestures_DirectionalNavigationGestures), [EditorCommands](Nodify_EditorCommands), [EditorGestures.GroupingNodeGestures](Nodify_Interactivity_EditorGestures_GroupingNodeGestures), [InputGestureRefExtensions](Nodify_Interactivity_InputGestureRefExtensions), [EditorGestures.ItemContainerGestures](Nodify_Interactivity_EditorGestures_ItemContainerGestures), [NodifyEditorGestures.KeyboardGestures](Nodify_Interactivity_NodifyEditorGestures_KeyboardGestures), [EditorGestures.MinimapGestures](Nodify_Interactivity_EditorGestures_MinimapGestures), [EditorGestures.NodifyEditorGestures](Nodify_Interactivity_EditorGestures_NodifyEditorGestures), [EditorGestures.SelectionGestures](Nodify_Interactivity_EditorGestures_SelectionGestures) 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. [EditorCommands](Nodify_EditorCommands)) diff --git a/docs/api/Nodify_Interactivity_InputGestureRefExtensions.md b/docs/api/Nodify_Interactivity_InputGestureRefExtensions.md new file mode 100644 index 00000000..e8993055 --- /dev/null +++ b/docs/api/Nodify_Interactivity_InputGestureRefExtensions.md @@ -0,0 +1,34 @@ +# InputGestureRefExtensions Class + +**Namespace:** Nodify.Interactivity + +**Assembly:** Nodify + +**Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [InputGestureRefExtensions](Nodify_Interactivity_InputGestureRefExtensions) + +**References:** [InputGestureRef](Nodify_Interactivity_InputGestureRef) + +Extension methods for the [InputGestureRef](Nodify_Interactivity_InputGestureRef) class. + +```csharp +public static class InputGestureRefExtensions +``` + +## Methods + +### AsRef(InputGesture) + +Creates a new [InputGestureRef](Nodify_Interactivity_InputGestureRef) from the specified gesture. + +```csharp +public static InputGestureRef AsRef(InputGesture gesture); +``` + +**Parameters** + +`gesture` [InputGesture](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.InputGesture) + +**Returns** + +[InputGestureRef](Nodify_Interactivity_InputGestureRef) + diff --git a/docs/api/Nodify_Interactivity_InputProcessor.md b/docs/api/Nodify_Interactivity_InputProcessor.md index 61377421..54e1ac3c 100644 --- a/docs/api/Nodify_Interactivity_InputProcessor.md +++ b/docs/api/Nodify_Interactivity_InputProcessor.md @@ -26,18 +26,6 @@ public InputProcessor(); ## Properties -### ProcessHandledEvents - -Gets or sets a value indicating whether events that have been handled should be processed. - -```csharp -public bool ProcessHandledEvents { get; set; } -``` - -**Property Value** - -[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) - ### RequiresInputCapture Gets a value indicating whether the processor has ongoing interactions that require input capture to remain active. diff --git a/docs/api/Nodify_Interactivity_KeyComboGesture.md b/docs/api/Nodify_Interactivity_KeyComboGesture.md new file mode 100644 index 00000000..9d302b8d --- /dev/null +++ b/docs/api/Nodify_Interactivity_KeyComboGesture.md @@ -0,0 +1,107 @@ +# KeyComboGesture Class + +**Namespace:** Nodify.Interactivity + +**Assembly:** Nodify + +**Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [InputGesture](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.InputGesture) → [KeyGesture](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.KeyGesture) → [KeyComboGesture](Nodify_Interactivity_KeyComboGesture) + +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. + +```csharp +public class KeyComboGesture : KeyGesture +``` + +## Constructors + +### KeyComboGesture(Key, Key) + +Initializes a new instance of the [KeyComboGesture](Nodify_Interactivity_KeyComboGesture) class with the specified trigger key, + combo key, modifiers, and display string. + +```csharp +public KeyComboGesture(Key triggerKey, Key comboKey); +``` + +**Parameters** + +`triggerKey` [Key](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.Key): The key that must be pressed first. + +`comboKey` [Key](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.Key): The combo key pressed while the trigger key is held. + +### KeyComboGesture(Key, Key, ModifierKeys) + +```csharp +public KeyComboGesture(Key triggerKey, Key comboKey, ModifierKeys modifiers); +``` + +**Parameters** + +`triggerKey` [Key](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.Key) + +`comboKey` [Key](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.Key) + +`modifiers` [ModifierKeys](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.ModifierKeys) + +### KeyComboGesture(Key, Key, ModifierKeys, String) + +```csharp +public KeyComboGesture(Key triggerKey, Key comboKey, ModifierKeys modifiers, string displayString); +``` + +**Parameters** + +`triggerKey` [Key](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.Key) + +`comboKey` [Key](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.Key) + +`modifiers` [ModifierKeys](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.ModifierKeys) + +`displayString` [String](https://docs.microsoft.com/en-us/dotnet/api/System.String) + +## Properties + +### AllowRepeatingComboKey + +Gets or sets a value indicating whether the combo key can be repeatedly triggered + without releasing the trigger key. + +```csharp +public bool AllowRepeatingComboKey { get; set; } +``` + +**Property Value** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### TriggerKey + +Gets or sets the key that must be pressed first to activate this combo gesture. + +```csharp +public Key TriggerKey { get; set; } +``` + +**Property Value** + +[Key](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.Key) + +## Methods + +### Matches(Object, InputEventArgs) + +```csharp +public override bool Matches(object targetElement, InputEventArgs inputEventArgs); +``` + +**Parameters** + +`targetElement` [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) + +`inputEventArgs` [InputEventArgs](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.InputEventArgs) + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + diff --git a/docs/api/Nodify_Interactivity_KeyboardNavigationLayerId.md b/docs/api/Nodify_Interactivity_KeyboardNavigationLayerId.md new file mode 100644 index 00000000..e2da177e --- /dev/null +++ b/docs/api/Nodify_Interactivity_KeyboardNavigationLayerId.md @@ -0,0 +1,56 @@ +# KeyboardNavigationLayerId Class + +**Namespace:** Nodify.Interactivity + +**Assembly:** Nodify + +**Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId) + +**References:** [ConnectionsMultiSelector](Nodify_ConnectionsMultiSelector), [DecoratorsControl](Nodify_DecoratorsControl), [IKeyboardNavigationLayer](Nodify_Interactivity_IKeyboardNavigationLayer), [IKeyboardNavigationLayerGroup](Nodify_Interactivity_IKeyboardNavigationLayerGroup), [NodifyEditor](Nodify_NodifyEditor) + +Represents a unique identifier for a keyboard navigation layer. + +```csharp +public class KeyboardNavigationLayerId +``` + +## Constructors + +### KeyboardNavigationLayerId() + +```csharp +public KeyboardNavigationLayerId(); +``` + +## Fields + +### Connections + +```csharp +public static KeyboardNavigationLayerId Connections; +``` + +**Field Value** + +[KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId) + +### Decorators + +```csharp +public static KeyboardNavigationLayerId Decorators; +``` + +**Field Value** + +[KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId) + +### Nodes + +```csharp +public static KeyboardNavigationLayerId Nodes; +``` + +**Field Value** + +[KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId) + diff --git a/docs/api/Nodify_Interactivity_MinimapState_KeyboardNavigation.md b/docs/api/Nodify_Interactivity_MinimapState_KeyboardNavigation.md new file mode 100644 index 00000000..b02c274a --- /dev/null +++ b/docs/api/Nodify_Interactivity_MinimapState_KeyboardNavigation.md @@ -0,0 +1,38 @@ +# MinimapState.KeyboardNavigation Class + +**Namespace:** Nodify.Interactivity + +**Assembly:** Nodify + +**Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [InputElementState\](Nodify_Interactivity_InputElementState_TElement_) → [MinimapState.KeyboardNavigation](Nodify_Interactivity_MinimapState_KeyboardNavigation) + +**References:** [Minimap](Nodify_Minimap) + +```csharp +public class KeyboardNavigation : InputElementState +``` + +## Constructors + +### MinimapState.KeyboardNavigation(Minimap) + +```csharp +public KeyboardNavigation(Minimap element); +``` + +**Parameters** + +`element` [Minimap](Nodify_Minimap) + +## Methods + +### OnKeyDown(KeyEventArgs) + +```csharp +protected override void OnKeyDown(KeyEventArgs e); +``` + +**Parameters** + +`e` [KeyEventArgs](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.KeyEventArgs) + diff --git a/docs/api/Nodify_Interactivity_MinimapState_Zooming.md b/docs/api/Nodify_Interactivity_MinimapState_Zooming.md index f05e5f8c..830293e4 100644 --- a/docs/api/Nodify_Interactivity_MinimapState_Zooming.md +++ b/docs/api/Nodify_Interactivity_MinimapState_Zooming.md @@ -26,6 +26,16 @@ public Zooming(Minimap minimap); ## Methods +### OnKeyDown(KeyEventArgs) + +```csharp +protected override void OnKeyDown(KeyEventArgs e); +``` + +**Parameters** + +`e` [KeyEventArgs](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.KeyEventArgs) + ### OnMouseWheel(MouseWheelEventArgs) ```csharp diff --git a/docs/api/Nodify_Interactivity_NodifyEditorGestures_KeyboardGestures.md b/docs/api/Nodify_Interactivity_NodifyEditorGestures_KeyboardGestures.md new file mode 100644 index 00000000..ec769799 --- /dev/null +++ b/docs/api/Nodify_Interactivity_NodifyEditorGestures_KeyboardGestures.md @@ -0,0 +1,112 @@ +# NodifyEditorGestures.KeyboardGestures Class + +**Namespace:** Nodify.Interactivity + +**Assembly:** Nodify + +**Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [NodifyEditorGestures.KeyboardGestures](Nodify_Interactivity_NodifyEditorGestures_KeyboardGestures) + +**References:** [EditorGestures.DirectionalNavigationGestures](Nodify_Interactivity_EditorGestures_DirectionalNavigationGestures), [InputGestureRef](Nodify_Interactivity_InputGestureRef), [EditorGestures.NodifyEditorGestures](Nodify_Interactivity_EditorGestures_NodifyEditorGestures) + +```csharp +public class KeyboardGestures +``` + +## Constructors + +### NodifyEditorGestures.KeyboardGestures() + +```csharp +public KeyboardGestures(); +``` + +## Properties + +### DeselectAll + +```csharp +public InputGestureRef DeselectAll { get; set; } +``` + +**Property Value** + +[InputGestureRef](Nodify_Interactivity_InputGestureRef) + +### DragSelection + +```csharp +public DirectionalNavigationGestures DragSelection { get; set; } +``` + +**Property Value** + +[EditorGestures.DirectionalNavigationGestures](Nodify_Interactivity_EditorGestures_DirectionalNavigationGestures) + +### NavigateSelection + +```csharp +public DirectionalNavigationGestures NavigateSelection { get; set; } +``` + +**Property Value** + +[EditorGestures.DirectionalNavigationGestures](Nodify_Interactivity_EditorGestures_DirectionalNavigationGestures) + +### NextNavigationLayer + +```csharp +public InputGestureRef NextNavigationLayer { get; set; } +``` + +**Property Value** + +[InputGestureRef](Nodify_Interactivity_InputGestureRef) + +### Pan + +```csharp +public DirectionalNavigationGestures Pan { get; set; } +``` + +**Property Value** + +[EditorGestures.DirectionalNavigationGestures](Nodify_Interactivity_EditorGestures_DirectionalNavigationGestures) + +### PrevNavigationLayer + +```csharp +public InputGestureRef PrevNavigationLayer { get; set; } +``` + +**Property Value** + +[InputGestureRef](Nodify_Interactivity_InputGestureRef) + +### ToggleSelected + +```csharp +public InputGestureRef ToggleSelected { get; set; } +``` + +**Property Value** + +[InputGestureRef](Nodify_Interactivity_InputGestureRef) + +## Methods + +### Apply(NodifyEditorGestures.KeyboardGestures) + +```csharp +public void Apply(NodifyEditorGestures.KeyboardGestures gestures); +``` + +**Parameters** + +`gestures` [NodifyEditorGestures.KeyboardGestures](Nodify_Interactivity_NodifyEditorGestures_KeyboardGestures) + +### Unbind() + +```csharp +public void Unbind(); +``` + diff --git a/docs/api/Nodify_ItemContainer.md b/docs/api/Nodify_ItemContainer.md index bb60e78c..5356d217 100644 --- a/docs/api/Nodify_ItemContainer.md +++ b/docs/api/Nodify_ItemContainer.md @@ -6,14 +6,14 @@ **Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [DispatcherObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Threading.DispatcherObject) → [DependencyObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyObject) → [Visual](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Media.Visual) → [UIElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.UIElement) → [FrameworkElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.FrameworkElement) → [Control](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.Control) → [ContentControl](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.ContentControl) → [ItemContainer](Nodify_ItemContainer) -**Implements:** [INodifyCanvasItem](Nodify_INodifyCanvasItem) +**Implements:** [INodifyCanvasItem](Nodify_INodifyCanvasItem), [IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_) **References:** [Connector](Nodify_Connector), [ContainerState.Default](Nodify_Interactivity_ContainerState_Default), [EditorCommands](Nodify_EditorCommands), [GroupingNode](Nodify_GroupingNode), [InputProcessor](Nodify_Interactivity_InputProcessor), [ItemsMovedEventArgs](Nodify_Events_ItemsMovedEventArgs), [NodifyEditor](Nodify_NodifyEditor), [PendingConnection](Nodify_PendingConnection), [PreviewLocationChanged](Nodify_Events_PreviewLocationChanged), [SelectionType](Nodify_SelectionType) The container for all the items generated by the [ItemsControl.ItemsSource](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.ItemsControl#itemssource) of the [NodifyEditor](Nodify_NodifyEditor). ```csharp -public class ItemContainer : ContentControl, INodifyCanvasItem +public class ItemContainer : ContentControl, INodifyCanvasItem, IKeyboardFocusTarget ``` ## Constructors @@ -56,6 +56,18 @@ public Size ActualSize { get; set; } [Size](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Size) +### Bounds + +Gets the bounds of the selection area for this [ItemContainer](Nodify_ItemContainer) based on its [ItemContainer.Location](Nodify_ItemContainer#location) and [ItemContainer.DesiredSizeForSelection](Nodify_ItemContainer#desiredsizeforselection). + +```csharp +public virtual Rect Bounds { get; set; } +``` + +**Property Value** + +[Rect](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Rect) + ### DesiredSizeForSelection Overrides the size to check against when calculating if this [ItemContainer](Nodify_ItemContainer) can be part of the current [NodifyEditor.SelectedArea](Nodify_NodifyEditor#selectedarea). @@ -387,6 +399,16 @@ protected void OnSelectedChanged(bool newValue); `newValue` [Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean): True if selected, false otherwise. +### OnVisualParentChanged(DependencyObject) + +```csharp +protected override void OnVisualParentChanged(DependencyObject oldParent); +``` + +**Parameters** + +`oldParent` [DependencyObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyObject) + ### Select(SelectionType) Modifies the selection state of the current item based on the specified selection type. diff --git a/docs/api/Nodify_Minimap.md b/docs/api/Nodify_Minimap.md index 684f5f37..06d7dbb9 100644 --- a/docs/api/Nodify_Minimap.md +++ b/docs/api/Nodify_Minimap.md @@ -6,7 +6,7 @@ **Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [DispatcherObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Threading.DispatcherObject) → [DependencyObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyObject) → [Visual](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Media.Visual) → [UIElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.UIElement) → [FrameworkElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.FrameworkElement) → [Control](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.Control) → [ItemsControl](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.ItemsControl) → [Minimap](Nodify_Minimap) -**References:** [InputProcessor](Nodify_Interactivity_InputProcessor), [MinimapItem](Nodify_MinimapItem), [NodifyEditor](Nodify_NodifyEditor), [MinimapState.Panning](Nodify_Interactivity_MinimapState_Panning), [ZoomEventArgs](Nodify_Events_ZoomEventArgs), [ZoomEventHandler](Nodify_Events_ZoomEventHandler), [MinimapState.Zooming](Nodify_Interactivity_MinimapState_Zooming) +**References:** [InputProcessor](Nodify_Interactivity_InputProcessor), [MinimapState.KeyboardNavigation](Nodify_Interactivity_MinimapState_KeyboardNavigation), [MinimapItem](Nodify_MinimapItem), [NodifyEditor](Nodify_NodifyEditor), [MinimapState.Panning](Nodify_Interactivity_MinimapState_Panning), [ZoomEventArgs](Nodify_Events_ZoomEventArgs), [ZoomEventHandler](Nodify_Events_ZoomEventHandler), [MinimapState.Zooming](Nodify_Interactivity_MinimapState_Zooming) A minimap control that can position the viewport, and zoom in and out. @@ -130,6 +130,18 @@ public Point MouseLocation { get; set; } [Point](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Point) +### NavigationStepSize + +Defines the distance to pan when using directional input (such as arrow keys). + +```csharp +public static double NavigationStepSize { get; set; } +``` + +**Property Value** + +[Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double) + ### ResizeToViewport Whether the minimap should resize to also display the whole viewport. @@ -327,6 +339,12 @@ protected override void OnMouseWheel(MouseWheelEventArgs e); `e` [MouseWheelEventArgs](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.MouseWheelEventArgs) +### ResetViewport() + +```csharp +public void ResetViewport(); +``` + ### SetViewportLocation(Point) ```csharp @@ -349,6 +367,18 @@ public void UpdatePanning(Point location); `location` [Point](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Point): The location to pan the viewport to. +### UpdatePanning(Vector) + +Pans the viewport by the specified amount. + +```csharp +public void UpdatePanning(Vector amount); +``` + +**Parameters** + +`amount` [Vector](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Vector): The amount to pan the viewport. + ### ZoomAtPosition(Double, Point) Zoom at the specified location in graph space coordinates. @@ -363,6 +393,22 @@ public void ZoomAtPosition(double zoom, Point location); `location` [Point](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Point): The location to focus when zooming. +### ZoomIn() + +Zoom in at the viewport's center. + +```csharp +public void ZoomIn(); +``` + +### ZoomOut() + +Zoom out at the viewport's center. + +```csharp +public void ZoomOut(); +``` + ## Events ### Zoom diff --git a/docs/api/Nodify_NodifyEditor.md b/docs/api/Nodify_NodifyEditor.md index 410f6049..43f7c2ec 100644 --- a/docs/api/Nodify_NodifyEditor.md +++ b/docs/api/Nodify_NodifyEditor.md @@ -6,14 +6,14 @@ **Inheritance:** [Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object) → [DispatcherObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Threading.DispatcherObject) → [DependencyObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyObject) → [Visual](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Media.Visual) → [UIElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.UIElement) → [FrameworkElement](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.FrameworkElement) → [Control](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.Control) → [ItemsControl](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.ItemsControl) → [Selector](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.Primitives.Selector) → [MultiSelector](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.Primitives.MultiSelector) → [NodifyEditor](Nodify_NodifyEditor) -**Implements:** [IScrollInfo](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.Primitives.IScrollInfo) +**Implements:** [IKeyboardNavigationLayer](Nodify_Interactivity_IKeyboardNavigationLayer), [IKeyboardNavigationLayerGroup](Nodify_Interactivity_IKeyboardNavigationLayerGroup), [IScrollInfo](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Controls.Primitives.IScrollInfo) -**References:** [Alignment](Nodify_Alignment), [BaseConnection](Nodify_BaseConnection), [Connection](Nodify_Connection), [Connector](Nodify_Connector), [EditorState.Cutting](Nodify_Interactivity_EditorState_Cutting), [CuttingLine](Nodify_CuttingLine), [DecoratorContainer](Nodify_DecoratorContainer), [EditorCommands](Nodify_EditorCommands), [EditorGestures](Nodify_Interactivity_EditorGestures), [GroupingNode](Nodify_GroupingNode), [InputProcessor](Nodify_Interactivity_InputProcessor), [ItemContainer](Nodify_ItemContainer), [ItemsMovedEventArgs](Nodify_Events_ItemsMovedEventArgs), [ItemsMovedEventHandler](Nodify_Events_ItemsMovedEventHandler), [Minimap](Nodify_Minimap), [EditorState.Panning](Nodify_Interactivity_EditorState_Panning), [EditorState.PanningWithMouseWheel](Nodify_Interactivity_EditorState_PanningWithMouseWheel), [PendingConnection](Nodify_PendingConnection), [EditorState.PushingItems](Nodify_Interactivity_EditorState_PushingItems), [EditorState.Selecting](Nodify_Interactivity_EditorState_Selecting), [SelectionType](Nodify_SelectionType), [EditorState.Zooming](Nodify_Interactivity_EditorState_Zooming) +**References:** [Alignment](Nodify_Alignment), [BaseConnection](Nodify_BaseConnection), [Connection](Nodify_Connection), [ConnectionsMultiSelector](Nodify_ConnectionsMultiSelector), [Connector](Nodify_Connector), [EditorState.Cutting](Nodify_Interactivity_EditorState_Cutting), [CuttingLine](Nodify_CuttingLine), [DecoratorContainer](Nodify_DecoratorContainer), [DecoratorsControl](Nodify_DecoratorsControl), [EditorCommands](Nodify_EditorCommands), [EditorGestures](Nodify_Interactivity_EditorGestures), [GroupingNode](Nodify_GroupingNode), [IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_), [InputProcessor](Nodify_Interactivity_InputProcessor), [ItemContainer](Nodify_ItemContainer), [ItemsMovedEventArgs](Nodify_Events_ItemsMovedEventArgs), [ItemsMovedEventHandler](Nodify_Events_ItemsMovedEventHandler), [EditorState.KeyboardNavigation](Nodify_Interactivity_EditorState_KeyboardNavigation), [KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId), [Minimap](Nodify_Minimap), [EditorState.Panning](Nodify_Interactivity_EditorState_Panning), [EditorState.PanningWithMouseWheel](Nodify_Interactivity_EditorState_PanningWithMouseWheel), [PendingConnection](Nodify_PendingConnection), [EditorState.PushingItems](Nodify_Interactivity_EditorState_PushingItems), [EditorState.Selecting](Nodify_Interactivity_EditorState_Selecting), [SelectionType](Nodify_SelectionType), [EditorState.Zooming](Nodify_Interactivity_EditorState_Zooming) Groups [ItemContainer](Nodify_ItemContainer)s and [Connection](Nodify_Connection)s in an area that you can drag, zoom and select. ```csharp -public class NodifyEditor : MultiSelector, IScrollInfo +public class NodifyEditor : MultiSelector, IKeyboardNavigationLayer, IKeyboardNavigationLayerGroup, IScrollInfo ``` ## Constructors @@ -196,6 +196,16 @@ protected static DependencyPropertyKey ViewportTransformPropertyKey; ## Properties +### ActiveNavigationLayer + +```csharp +public virtual IKeyboardNavigationLayer ActiveNavigationLayer { get; set; } +``` + +**Property Value** + +[IKeyboardNavigationLayer](Nodify_Interactivity_IKeyboardNavigationLayer) + ### AllowCuttingCancellation Gets or sets whether cancelling a cutting operation is allowed (see Nodify.Interactivity.EditorGestures.NodifyEditorGestures.CancelAction). @@ -256,6 +266,18 @@ public static bool AllowSelectionCancellation { get; set; } [Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) +### AutoFocusFirstElement + +Automatically focus the first container when the navigation layer changes or the editor gets focused. + +```csharp +public static bool AutoFocusFirstElement { get; set; } +``` + +**Property Value** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + ### AutoPanEdgeDistance Gets or sets the maximum distance in pixels from the edge of the editor that will trigger auto-panning. @@ -280,6 +302,18 @@ public static double AutoPanningTickRate { get; set; } [Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double) +### AutoPanOnNodeFocus + +Automatically pan the viewport when a node is focused via keyboard navigation. + +```csharp +public static bool AutoPanOnNodeFocus { get; set; } +``` + +**Property Value** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + ### AutoPanSpeed Gets or sets the speed used when auto-panning scaled by [NodifyEditor.AutoPanningTickRate](Nodify_NodifyEditor#autopanningtickrate) @@ -292,6 +326,42 @@ public double AutoPanSpeed { get; set; } [Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double) +### AutoRegisterConnectionsLayer + +Automatically registers the connectors layer for keyboard navigation. + +```csharp +public static bool AutoRegisterConnectionsLayer { get; set; } +``` + +**Property Value** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### AutoRegisterDecoratorsLayer + +Automatically registers the decorators layer for keyboard navigation. + +```csharp +public static bool AutoRegisterDecoratorsLayer { get; set; } +``` + +**Property Value** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### BringIntoViewEdgeOffset + +Gets or sets the default viewport edge offset applied when bringing an item into view as a result of keyboard focus. + +```csharp +public static double BringIntoViewEdgeOffset { get; set; } +``` + +**Property Value** + +[Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double) + ### BringIntoViewMaxDuration Gets or sets the maximum animation duration in seconds for bringing a location into view. @@ -813,6 +883,16 @@ public ICommand ItemsSelectStartedCommand { get; set; } [ICommand](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.ICommand) +### KeyboardNavigationLayer + +```csharp +public IKeyboardNavigationLayer KeyboardNavigationLayer { get; set; } +``` + +**Property Value** + +[IKeyboardNavigationLayer](Nodify_Interactivity_IKeyboardNavigationLayer) + ### MaxViewportZoom Gets or sets the maximum zoom factor of the viewport @@ -825,6 +905,19 @@ public double MaxViewportZoom { get; set; } [Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double) +### MinimumNavigationStepSize + +Defines the minimum distance to move or navigate when using directional input (such as arrow keys), scaled by the [NodifyEditor.ViewportZoom](Nodify_NodifyEditor#viewportzoom). + If the [NodifyEditor.GridCellSize](Nodify_NodifyEditor#gridcellsize) is smaller than this value, the movement step is increased to the nearest greater multiple of the [NodifyEditor.GridCellSize](Nodify_NodifyEditor#gridcellsize). + +```csharp +public static double MinimumNavigationStepSize { get; set; } +``` + +**Property Value** + +[Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double) + ### MinViewportZoom Gets or sets the minimum zoom factor of the viewport @@ -887,6 +980,18 @@ public static double OptimizeRenderingZoomOutPercent { get; set; } [Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double) +### PanViewportOnKeyboardDrag + +Indicates whether the viewport should automatically pan to follow elements moved via keyboard dragging. + +```csharp +public static bool PanViewportOnKeyboardDrag { get; set; } +``` + +**Property Value** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + ### PendingConnection Gets of sets the [FrameworkElement.DataContext](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.FrameworkElement#datacontext) of the [PendingConnection](Nodify_PendingConnection). @@ -1095,6 +1200,40 @@ public double ViewportZoom { get; set; } ## Methods +### ActivateNavigationLayer(KeyboardNavigationLayerId) + +```csharp +public virtual bool ActivateNavigationLayer(KeyboardNavigationLayerId layerId); +``` + +**Parameters** + +`layerId` [KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId) + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### ActivateNextNavigationLayer() + +```csharp +public virtual bool ActivateNextNavigationLayer(); +``` + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### ActivatePreviousNavigationLayer() + +```csharp +public virtual bool ActivatePreviousNavigationLayer(); +``` + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + ### AlignContainers(IEnumerable\, Alignment, ItemContainer) Aligns a collection of containers based on the specified alignment. @@ -1237,7 +1376,7 @@ public void BringIntoView(Point point, bool animated = true, Action onFinish = n ### BringIntoView(Rect) -Moves the viewport center at the center of the specified area. +Ensures the specified item container is fully visible within the viewport, optionally with padding around the edges. ```csharp public void BringIntoView(Rect area); @@ -1247,6 +1386,18 @@ public void BringIntoView(Rect area); `area` [Rect](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Rect): The location in graph space coordinates. +### BringIntoView(Rect, Double) + +```csharp +public void BringIntoView(Rect area, double offsetFromEdge = 32d); +``` + +**Parameters** + +`area` [Rect](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Rect) + +`offsetFromEdge` [Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double) + ### CancelCutting() Cancels the current cutting operation without applying any changes if [NodifyEditor.AllowCuttingCancellation](Nodify_NodifyEditor#allowcuttingcancellation) is true. @@ -1332,6 +1483,22 @@ Completes the selection operation and applies any pending changes. public void EndSelecting(); ``` +### FindNextFocusTarget(ItemContainer, TraversalRequest) + +```csharp +protected virtual ItemContainer FindNextFocusTarget(ItemContainer currentContainer, TraversalRequest request); +``` + +**Parameters** + +`currentContainer` [ItemContainer](Nodify_ItemContainer) + +`request` [TraversalRequest](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.TraversalRequest) + +**Returns** + +[ItemContainer](Nodify_ItemContainer) + ### FitToScreen(Rect?) Scales the viewport to fit the specified area or all the [ItemContainer](Nodify_ItemContainer)s if that's possible. @@ -1354,6 +1521,16 @@ protected override DependencyObject GetContainerForItemOverride(); [DependencyObject](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.DependencyObject) +### GetEnumerator() + +```csharp +public virtual IEnumerator GetEnumerator(); +``` + +**Returns** + +[IEnumerator\](https://docs.microsoft.com/en-us/dotnet/api/System.Collections.Generic.IEnumerator-1) + ### GetLocationInsideEditor(Point, UIElement) Translates the specified location to graph space coordinates (relative to the [NodifyEditor.ItemsHost](Nodify_NodifyEditor#itemshost)). @@ -1440,12 +1617,70 @@ Locks the position of the [NodifyEditor.SelectedContainers](Nodify_NodifyEditor# public void LockSelection(); ``` +### MoveFocus(FocusNavigationDirection) + +```csharp +public bool MoveFocus(FocusNavigationDirection direction); +``` + +**Parameters** + +`direction` [FocusNavigationDirection](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.FocusNavigationDirection) + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### MoveFocus(TraversalRequest) + +```csharp +public bool MoveFocus(TraversalRequest request); +``` + +**Parameters** + +`request` [TraversalRequest](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.TraversalRequest) + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + ### OnApplyTemplate() ```csharp public override void OnApplyTemplate(); ``` +### OnElementFocused(IKeyboardFocusTarget\) + +```csharp +protected virtual void OnElementFocused(IKeyboardFocusTarget target); +``` + +**Parameters** + +`target` [IKeyboardFocusTarget\](Nodify_Interactivity_IKeyboardFocusTarget_TElement_) + +### OnGotKeyboardFocus(KeyboardFocusChangedEventArgs) + +```csharp +protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e); +``` + +**Parameters** + +`e` [KeyboardFocusChangedEventArgs](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.KeyboardFocusChangedEventArgs) + +### OnKeyboardNavigationLayerActivated(IKeyboardNavigationLayer) + +```csharp +protected virtual void OnKeyboardNavigationLayerActivated(IKeyboardNavigationLayer activeLayer); +``` + +**Parameters** + +`activeLayer` [IKeyboardNavigationLayer](Nodify_Interactivity_IKeyboardNavigationLayer) + ### OnKeyDown(KeyEventArgs) ```csharp @@ -1466,6 +1701,16 @@ protected override void OnKeyUp(KeyEventArgs e); `e` [KeyEventArgs](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.KeyEventArgs) +### OnLostKeyboardFocus(KeyboardFocusChangedEventArgs) + +```csharp +protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e); +``` + +**Parameters** + +`e` [KeyboardFocusChangedEventArgs](https://docs.microsoft.com/en-us/dotnet/api/System.Windows.Input.KeyboardFocusChangedEventArgs) + ### OnLostMouseCapture(MouseEventArgs) ```csharp @@ -1555,6 +1800,48 @@ Updates the [NodifyEditor.ViewportSize](Nodify_NodifyEditor#viewportsize) and ra protected void OnViewportUpdated(); ``` +### RegisterNavigationLayer(IKeyboardNavigationLayer) + +```csharp +public virtual bool RegisterNavigationLayer(IKeyboardNavigationLayer layer); +``` + +**Parameters** + +`layer` [IKeyboardNavigationLayer](Nodify_Interactivity_IKeyboardNavigationLayer) + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### RemoveNavigationLayer(KeyboardNavigationLayerId) + +```csharp +public virtual bool RemoveNavigationLayer(KeyboardNavigationLayerId layerId); +``` + +**Parameters** + +`layerId` [KeyboardNavigationLayerId](Nodify_Interactivity_KeyboardNavigationLayerId) + +**Returns** + +[Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean) + +### ResetViewport(Boolean, Action) + +Reset the viewport location to (0, 0) and the viewport zoom to 1. + +```csharp +public void ResetViewport(bool animated = true, Action onFinish = null); +``` + +**Parameters** + +`animated` [Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean): Whether the viewport transition is animated. + +`onFinish` [Action](https://docs.microsoft.com/en-us/dotnet/api/System.Action): The callback invoked when the viewport transition is finished. + ### Select(ItemContainer) Clears the current selection and selects the specified [ItemContainer](Nodify_ItemContainer) within the same selection transaction. @@ -1737,7 +2024,7 @@ public void ZoomAtPosition(double zoom, Point location); ### ZoomIn() -Zoom in at the viewports center +Zoom in at the viewport's center. ```csharp public void ZoomIn(); @@ -1745,7 +2032,7 @@ public void ZoomIn(); ### ZoomOut() -Zoom out at the viewports center +Zoom out at the viewport's center. ```csharp public void ZoomOut(); @@ -1753,6 +2040,16 @@ public void ZoomOut(); ## Events +### ActiveNavigationLayerChanged + +```csharp +public virtual event Action ActiveNavigationLayerChanged; +``` + +**Event Type** + +[Action\](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1) + ### ItemsMoved Occurs when items are moved within the editor (see Nodify.NodifyEditor.BeginDragging, Nodify.NodifyEditor.BeginPushingItems(System.Windows.Point,System.Windows.Controls.Orientation)). diff --git a/docs/api/Nodify_SelectionType.md b/docs/api/Nodify_SelectionType.md index 9633e2a2..32d43379 100644 --- a/docs/api/Nodify_SelectionType.md +++ b/docs/api/Nodify_SelectionType.md @@ -4,7 +4,7 @@ **Assembly:** Nodify -**References:** [ItemContainer](Nodify_ItemContainer), [NodifyEditor](Nodify_NodifyEditor) +**References:** [ConnectionContainer](Nodify_ConnectionContainer), [ItemContainer](Nodify_ItemContainer), [NodifyEditor](Nodify_NodifyEditor) Available selection logic.