Add project files.

This commit is contained in:
Ankitkumar Satapara
2026-04-17 22:31:58 +05:30
commit 21aaef6776
473 changed files with 50152 additions and 0 deletions

View File

@@ -0,0 +1,233 @@
using Nodify.Interactivity;
using System;
using System.Linq;
using System.Windows;
using System.Windows.Input;
namespace Nodify
{
/// <summary>
/// Provides common commands for the <see cref="NodifyEditor"/>.
/// </summary>
public static class EditorCommands
{
/// <summary>
/// Zoom in relative to the editor's viewport center.
/// </summary>
public static RoutedUICommand ZoomIn { get; } = new RoutedUICommand("Zoom in", nameof(ZoomIn), typeof(EditorCommands), new InputGestureCollection
{
EditorGestures.Mappings.Editor.ZoomIn
});
/// <summary>
/// Zoom out relative to the editor's viewport center.
/// </summary>
public static RoutedUICommand ZoomOut { get; } = new RoutedUICommand("Zoom out", nameof(ZoomOut), typeof(EditorCommands), new InputGestureCollection
{
EditorGestures.Mappings.Editor.ZoomOut
});
/// <summary>
/// Select all <see cref="ItemContainer"/>s in the <see cref="NodifyEditor"/>.
/// </summary>
public static RoutedUICommand SelectAll { get; } = new RoutedUICommand(ApplicationCommands.SelectAll.Text, nameof(SelectAll), typeof(EditorCommands), new InputGestureCollection
{
EditorGestures.Mappings.Editor.SelectAll
});
/// <summary>
/// Moves the <see cref="NodifyEditor.ViewportLocation"/> to the specified location.
/// Parameter is a <see cref="Point"/> or a string that can be converted to a point.
/// </summary>
public static RoutedUICommand BringIntoView { get; } = new RoutedUICommand("Bring location into view", nameof(BringIntoView), typeof(EditorCommands), new InputGestureCollection
{
EditorGestures.Mappings.Editor.ResetViewport
});
/// <summary>
/// Scales the editor's viewport to fit all the <see cref="ItemContainer"/>s if that's possible.
/// </summary>
public static RoutedUICommand FitToScreen { get; } = new RoutedUICommand("Fit to screen", nameof(FitToScreen), typeof(EditorCommands), new InputGestureCollection
{
EditorGestures.Mappings.Editor.FitToScreen
});
/// <summary>
/// Aligns the <see cref="NodifyEditor.SelectedContainers"/> using the specified alignment method.
/// Parameter is of type <see cref="Alignment"/> or a string that can be converted to an alignment.
/// </summary>
public static RoutedUICommand Align { get; } = new RoutedUICommand("Align", nameof(Align), typeof(EditorCommands));
/// <summary>
/// Locks the position of the <see cref="NodifyEditor.SelectedContainers"/>.
/// </summary>
public static RoutedUICommand LockSelection { get; } = new RoutedUICommand("Lock selection", nameof(LockSelection), typeof(EditorCommands));
/// <summary>
/// Unlocks the position of the <see cref="NodifyEditor.SelectedContainers"/>.
/// </summary>
public static RoutedUICommand UnlockSelection { get; } = new RoutedUICommand("Unlock selection", nameof(UnlockSelection), typeof(EditorCommands));
internal static void RegisterCommandBindings<T>()
{
CommandManager.RegisterClassCommandBinding(typeof(T), new CommandBinding(ZoomIn, OnZoomIn, OnQueryStatusZoomIn));
CommandManager.RegisterClassCommandBinding(typeof(T), new CommandBinding(ZoomOut, OnZoomOut, OnQueryStatusZoomOut));
CommandManager.RegisterClassCommandBinding(typeof(T), new CommandBinding(SelectAll, OnSelectAll, OnQuerySelectAllStatus));
CommandManager.RegisterClassCommandBinding(typeof(T), new CommandBinding(BringIntoView, OnBringIntoView, OnQueryBringIntoViewStatus));
CommandManager.RegisterClassCommandBinding(typeof(T), new CommandBinding(FitToScreen, OnFitToScreen, OnQueryFitToScreenStatus));
CommandManager.RegisterClassCommandBinding(typeof(T), new CommandBinding(Align, OnAlign, OnQueryAlignStatus));
CommandManager.RegisterClassCommandBinding(typeof(T), new CommandBinding(LockSelection, OnLock, OnQueryLockStatus));
CommandManager.RegisterClassCommandBinding(typeof(T), new CommandBinding(UnlockSelection, OnUnlock, OnQueryUnlockStatus));
}
private static void OnQueryLockStatus(object sender, CanExecuteRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
e.CanExecute = editor.SelectedContainers.Any(x => x.IsDraggable);
}
}
private static void OnLock(object sender, ExecutedRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
editor.LockSelection();
}
}
private static void OnQueryUnlockStatus(object sender, CanExecuteRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
e.CanExecute = editor.SelectedContainers.Any(x => !x.IsDraggable);
}
}
private static void OnUnlock(object sender, ExecutedRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
editor.UnlockSelection();
}
}
private static void OnQueryAlignStatus(object sender, CanExecuteRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
e.CanExecute = editor.SelectedContainersCount > 1;
}
}
private static void OnAlign(object sender, ExecutedRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
if (e.Parameter is Alignment alignment)
{
editor.AlignSelection(alignment, e.OriginalSource as ItemContainer);
}
else if (e.Parameter is string str && Enum.TryParse(str, true, out alignment))
{
editor.AlignSelection(alignment, e.OriginalSource as ItemContainer);
}
else
{
editor.AlignSelection(Alignment.Top, e.OriginalSource as ItemContainer);
}
}
}
private static void OnQueryBringIntoViewStatus(object sender, CanExecuteRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
e.CanExecute = !editor.DisablePanning;
}
}
private static void OnBringIntoView(object sender, ExecutedRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
switch (e.Parameter)
{
case Point location:
editor.BringIntoView(location);
break;
case string str:
editor.BringIntoView(Point.Parse(str));
break;
default:
editor.ResetViewport();
break;
}
}
}
private static void OnQueryFitToScreenStatus(object sender, CanExecuteRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
e.CanExecute = editor.HasItems;
}
}
private static void OnFitToScreen(object sender, ExecutedRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
editor.FitToScreen();
}
}
private static void OnQuerySelectAllStatus(object sender, CanExecuteRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
e.CanExecute = !editor.IsSelecting && editor.CanSelectMultipleItems;
}
}
private static void OnSelectAll(object sender, ExecutedRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
editor.SelectAll();
}
}
private static void OnQueryStatusZoomIn(object sender, CanExecuteRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
e.CanExecute = editor.ViewportZoom < editor.MaxViewportZoom;
}
}
private static void OnZoomIn(object sender, ExecutedRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
editor.ZoomIn();
}
}
private static void OnQueryStatusZoomOut(object sender, CanExecuteRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
e.CanExecute = editor.ViewportZoom > editor.MinViewportZoom;
}
}
private static void OnZoomOut(object sender, ExecutedRoutedEventArgs e)
{
if (sender is NodifyEditor editor)
{
editor.ZoomOut();
}
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Windows;
namespace Nodify.Events
{
/// <summary>
/// Represents a method signature used to handle the <see cref="NodifyEditor.ItemsMovedEvent"/> routed event.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The event data containing information about the moved items and their offset.</param>
public delegate void ItemsMovedEventHandler(object sender, ItemsMovedEventArgs e);
/// <summary>
/// Provides data for the <see cref="NodifyEditor.ItemsMovedEvent"/> routed event.
/// </summary>
public class ItemsMovedEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemsMovedEventArgs"/> class with the specified moved items and offset.
/// </summary>
/// <param name="items">The collection of items that were moved.</param>
/// <param name="offset">The vector representing the distance the items were moved.</param>
public ItemsMovedEventArgs(IReadOnlyCollection<object> items, Vector offset)
{
Items = items;
Offset = offset;
}
/// <summary>
/// Gets or sets the vector representing the distance the items were moved.
/// </summary>
public Vector Offset { get; set; }
/// <summary>
/// Gets a collection of <see cref="FrameworkElement.DataContext"/>s of the <see cref="ItemContainer"/>s associated with this event.
/// </summary>
public IReadOnlyCollection<object> Items { get; }
protected override void InvokeEventHandler(Delegate genericHandler, object genericTarget)
=> ((ItemsMovedEventHandler)genericHandler)(genericTarget, this);
}
}

View File

@@ -0,0 +1,92 @@
using System.Windows;
using System.Windows.Controls;
namespace Nodify
{
/// <summary>Interface for items inside a <see cref="NodifyCanvas"/>.</summary>
public interface INodifyCanvasItem
{
/// <summary>The location of the item.</summary>
Point Location { get; }
/// <summary>The desired size of the item.</summary>
Size DesiredSize { get; }
/// <inheritdoc cref="UIElement.Arrange(Rect)" />
void Arrange(Rect rect);
}
/// <summary>A canvas like panel that works with <see cref="INodifyCanvasItem"/>s.</summary>
public class NodifyCanvas : Panel
{
public static readonly DependencyProperty ExtentProperty = DependencyProperty.Register(nameof(Extent), typeof(Rect), typeof(NodifyCanvas), new FrameworkPropertyMetadata(BoxValue.Rect));
/// <summary>The area covered by the children of this panel.</summary>
public Rect Extent
{
get => (Rect)GetValue(ExtentProperty);
set => SetValue(ExtentProperty, value);
}
/// <inheritdoc />
protected override Size ArrangeOverride(Size arrangeSize)
{
double minX = double.MaxValue;
double minY = double.MaxValue;
double maxX = double.MinValue;
double maxY = double.MinValue;
UIElementCollection children = InternalChildren;
for (int i = 0; i < children.Count; i++)
{
var item = (INodifyCanvasItem)children[i];
item.Arrange(new Rect(item.Location, item.DesiredSize));
Size size = children[i].RenderSize;
if (item.Location.X < minX)
{
minX = item.Location.X;
}
if (item.Location.Y < minY)
{
minY = item.Location.Y;
}
double sizeX = item.Location.X + size.Width;
if (sizeX > maxX)
{
maxX = sizeX;
}
double sizeY = item.Location.Y + size.Height;
if (sizeY > maxY)
{
maxY = sizeY;
}
}
Extent = minX == double.MaxValue
? new Rect(0, 0, 0, 0)
: new Rect(minX, minY, maxX - minX, maxY - minY);
return arrangeSize;
}
/// <inheritdoc />
protected override Size MeasureOverride(Size constraint)
{
var availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
UIElementCollection children = InternalChildren;
for (int i = 0; i < children.Count; i++)
{
children[i].Measure(availableSize);
}
return default;
}
}
}

View File

@@ -0,0 +1,266 @@
using System.Collections.Generic;
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Diagnostics;
using Nodify.Interactivity;
namespace Nodify
{
[StyleTypedProperty(Property = nameof(CuttingLineStyle), StyleTargetType = typeof(CuttingLine))]
public partial class NodifyEditor
{
#region Dependency properties
protected static readonly DependencyPropertyKey CuttingLineStartPropertyKey = DependencyProperty.RegisterReadOnly(nameof(CuttingLineStart), typeof(Point), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.Point));
public static readonly DependencyProperty CuttingLineStartProperty = CuttingLineStartPropertyKey.DependencyProperty;
protected static readonly DependencyPropertyKey CuttingLineEndPropertyKey = DependencyProperty.RegisterReadOnly(nameof(CuttingLineEnd), typeof(Point), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.Point));
public static readonly DependencyProperty CuttingLineEndProperty = CuttingLineEndPropertyKey.DependencyProperty;
protected static readonly DependencyPropertyKey IsCuttingPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsCutting), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False, OnIsCuttingChanged));
public static readonly DependencyProperty IsCuttingProperty = IsCuttingPropertyKey.DependencyProperty;
public static readonly DependencyProperty CuttingLineStyleProperty = DependencyProperty.Register(nameof(CuttingLineStyle), typeof(Style), typeof(NodifyEditor));
public static readonly DependencyProperty CuttingStartedCommandProperty = DependencyProperty.Register(nameof(CuttingStartedCommand), typeof(ICommand), typeof(NodifyEditor));
public static readonly DependencyProperty CuttingCompletedCommandProperty = DependencyProperty.Register(nameof(CuttingCompletedCommand), typeof(ICommand), typeof(NodifyEditor));
private static void OnIsCuttingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var editor = (NodifyEditor)d;
if ((bool)e.NewValue == true)
editor.OnCuttingStarted();
else
editor.OnCuttingCompleted();
}
private void OnCuttingCompleted()
{
if (CuttingCompletedCommand?.CanExecute(DataContext) ?? false)
CuttingCompletedCommand.Execute(DataContext);
}
private void OnCuttingStarted()
{
if (CuttingStartedCommand?.CanExecute(DataContext) ?? false)
CuttingStartedCommand.Execute(DataContext);
}
/// <summary>
/// Gets or sets the style to use for the cutting line.
/// </summary>
public Style CuttingLineStyle
{
get => (Style)GetValue(CuttingLineStyleProperty);
set => SetValue(CuttingLineStyleProperty, value);
}
/// <summary>
/// Gets the start point of the <see cref="CuttingLine"/> while <see cref="IsCutting"/> is true.
/// </summary>
public Point CuttingLineStart
{
get => (Point)GetValue(CuttingLineStartProperty);
private set => SetValue(CuttingLineStartPropertyKey, value);
}
/// <summary>
/// Gets the end point of the <see cref="CuttingLine"/> while <see cref="IsCutting"/> is true.
/// </summary>
public Point CuttingLineEnd
{
get => (Point)GetValue(CuttingLineEndProperty);
private set => SetValue(CuttingLineEndPropertyKey, value);
}
/// <summary>
/// Gets a value that indicates whether a cutting operation is in progress.
/// </summary>
public bool IsCutting
{
get => (bool)GetValue(IsCuttingProperty);
private set => SetValue(IsCuttingPropertyKey, value);
}
/// <summary>Invoked when a cutting operation is started.</summary>
public ICommand? CuttingStartedCommand
{
get => (ICommand?)GetValue(CuttingStartedCommandProperty);
set => SetValue(CuttingStartedCommandProperty, value);
}
/// <summary>Invoked when a cutting operation is completed.</summary>
public ICommand? CuttingCompletedCommand
{
get => (ICommand?)GetValue(CuttingCompletedCommandProperty);
set => SetValue(CuttingCompletedCommandProperty, value);
}
#endregion
/// <summary>
/// Gets or sets whether cancelling a cutting operation is allowed (see <see cref="EditorGestures.NodifyEditorGestures.CancelAction"/>).
/// </summary>
public static bool AllowCuttingCancellation { get; set; } = true;
/// <summary>
/// Gets or sets whether the cutting line should apply the preview style to the interesected elements.
/// </summary>
/// <remarks>
/// This may hurt performance because intersection must be calculated on mouse move.
/// </remarks>
public static bool EnableCuttingLinePreview { get; set; } = false;
/// <summary>
/// The list of supported connection types for cutting. Type must be derived from <see cref="FrameworkElement" />.
/// </summary>
public static readonly HashSet<Type> CuttingConnectionTypes = new HashSet<Type>();
private List<FrameworkElement>? _cuttingLinePreviousConnections;
private readonly LineGeometry _cuttingLineGeometry = new LineGeometry();
/// <summary>
/// Starts the cutting operation at the current <see cref="MouseLocation"/>. Call <see cref="EndCutting"/> to complete the operation or <see cref="CancelCutting"/> to abort it.
/// </summary>
/// <remarks>This method has no effect if a cutting operation is already in progress.</remarks>
public void BeginCutting()
=> BeginCutting(MouseLocation);
/// <summary>
/// Starts the cutting operation at the specified location. Call <see cref="EndCutting"/> to complete the operation or <see cref="CancelCutting"/> to abort it.
/// </summary>
/// <remarks>This method has no effect if a cutting operation is already in progress.</remarks>
/// <param name="location">The starting location for cutting items, in graph space coordinates.</param>
public void BeginCutting(Point location)
{
if (IsCutting)
{
return;
}
CuttingLineStart = location;
CuttingLineEnd = location;
IsCutting = true;
_cuttingLineGeometry.StartPoint = location;
_cuttingLineGeometry.EndPoint = location;
}
/// <summary>
/// Updates the current cutting line position and the style for the intersecting elements if <see cref="EnableCuttingLinePreview"/> is true.
/// </summary>
/// <param name="amount">The amount to adjust the cutting line's endpoint.</param>
public void UpdateCuttingLine(Vector amount)
{
CuttingLineEnd += amount;
UpdateCuttingLine(CuttingLineEnd);
}
/// <summary>
/// Updates the current cutting line position and the style for the intersecting elements if <see cref="EnableCuttingLinePreview"/> is true.
/// </summary>
/// <param name="location">The location of the cutting line's endpoint.</param>
public void UpdateCuttingLine(Point location)
{
Debug.Assert(IsCutting);
CuttingLineEnd = location;
if (EnableCuttingLinePreview)
{
_cuttingLineGeometry.EndPoint = CuttingLineEnd;
ResetConnectionStyle();
ApplyConnectionStyle();
}
}
/// <summary>
/// Cancels the current cutting operation without applying any changes if <see cref="AllowCuttingCancellation"/> is true.
/// Otherwise, it ends the cutting operation by calling <see cref="EndCutting"/>.
/// </summary>
/// <remarks>This method has no effect if there's no cutting operation in progress.</remarks>
public void CancelCutting()
{
if (!AllowCuttingCancellation)
{
EndCutting();
return;
}
if (IsCutting)
{
ResetConnectionStyle();
IsCutting = false;
}
}
/// <summary>
/// Completes the cutting operation and applies the changes.
/// </summary>
/// <remarks>This method has no effect if there's no cutting operation in progress.</remarks>
public void EndCutting()
{
if (!IsCutting)
{
return;
}
ResetConnectionStyle();
var lineGeometry = new LineGeometry(CuttingLineStart, CuttingLineEnd);
var connections = ConnectionsHost.GetIntersectingElements(lineGeometry, CuttingConnectionTypes);
if (RemoveConnectionCommand != null)
{
foreach (var connection in connections)
{
OnRemoveConnection(connection.DataContext);
}
}
else
{
RemoveSupportedConnections(connections);
}
IsCutting = false;
}
private static void RemoveSupportedConnections(List<FrameworkElement> connections)
{
foreach (var connection in connections)
{
if (connection is BaseConnection bc)
{
bc.Remove();
}
}
}
private void ApplyConnectionStyle()
{
var connections = ConnectionsHost.GetIntersectingElements(_cuttingLineGeometry, CuttingConnectionTypes);
foreach (var connection in connections)
{
CuttingLine.SetIsOverElement(connection, true);
}
_cuttingLinePreviousConnections = connections;
}
private void ResetConnectionStyle()
{
if (_cuttingLinePreviousConnections != null)
{
foreach (var connection in _cuttingLinePreviousConnections)
{
CuttingLine.SetIsOverElement(connection, false);
}
_cuttingLinePreviousConnections = null;
}
}
}
}

View File

@@ -0,0 +1,198 @@
using System.Windows.Input;
using System.Windows;
using System.Collections.Generic;
using System.Diagnostics;
using Nodify.Events;
using System.Linq;
namespace Nodify
{
public partial class NodifyEditor
{
#region Dependency properties
public static readonly DependencyProperty ItemsDragStartedCommandProperty = DependencyProperty.Register(nameof(ItemsDragStartedCommand), typeof(ICommand), typeof(NodifyEditor));
public static readonly DependencyProperty ItemsDragCompletedCommandProperty = DependencyProperty.Register(nameof(ItemsDragCompletedCommand), typeof(ICommand), typeof(NodifyEditor));
protected static readonly DependencyPropertyKey IsDraggingPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsDragging), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False, OnIsDraggingChanged));
public static readonly DependencyProperty IsDraggingProperty = IsDraggingPropertyKey.DependencyProperty;
private static void OnIsDraggingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var editor = (NodifyEditor)d;
if ((bool)e.NewValue == true)
{
editor.OnItemsDragStarted();
}
else
{
editor.OnItemsDragCompleted();
}
}
private void OnItemsDragCompleted()
{
if (ItemsDragCompletedCommand?.CanExecute(DataContext) ?? false)
ItemsDragCompletedCommand.Execute(DataContext);
}
private void OnItemsDragStarted()
{
if (ItemsDragStartedCommand?.CanExecute(DataContext) ?? false)
ItemsDragStartedCommand.Execute(DataContext);
}
/// <summary>
/// Invoked when a drag operation starts for the <see cref="SelectedContainers"/>, or when <see cref="IsPushingItems"/> is set to true.
/// </summary>
public ICommand? ItemsDragStartedCommand
{
get => (ICommand?)GetValue(ItemsDragStartedCommandProperty);
set => SetValue(ItemsDragStartedCommandProperty, value);
}
/// <summary>
/// Invoked when a drag operation is completed for the <see cref="SelectedContainers"/>, or when <see cref="IsPushingItems"/> is set to false.
/// </summary>
public ICommand? ItemsDragCompletedCommand
{
get => (ICommand?)GetValue(ItemsDragCompletedCommandProperty);
set => SetValue(ItemsDragCompletedCommandProperty, value);
}
/// <summary>
/// Gets a value that indicates whether a dragging operation is in progress.
/// </summary>
public bool IsDragging
{
get => (bool)GetValue(IsDraggingProperty);
private set => SetValue(IsDraggingPropertyKey, value);
}
#endregion
#region Routed events
public static readonly RoutedEvent ItemsMovedEvent = EventManager.RegisterRoutedEvent(nameof(ItemsMoved), RoutingStrategy.Bubble, typeof(ItemsMovedEventHandler), typeof(NodifyEditor));
/// <summary>
/// Occurs when items are moved within the editor (see <see cref="BeginDragging()"/>, <see cref="BeginPushingItems(Point, System.Windows.Controls.Orientation)"/>).
/// </summary>
public event ItemsMovedEventHandler ItemsMoved
{
add => AddHandler(ItemsMovedEvent, value);
remove => RemoveHandler(ItemsMovedEvent, value);
}
#endregion
/// <summary>
/// Gets or sets whether cancelling a dragging operation is allowed.
/// </summary>
public static bool AllowDraggingCancellation { get; set; } = true;
/// <summary>
/// Gets or sets if the current position of containers that are being dragged should not be committed until the end of the dragging operation.
/// </summary>
public static bool EnableDraggingContainersOptimizations { get; set; } = true;
private IDraggingStrategy? _draggingStrategy;
/// <summary>
/// Initiates the dragging operation using the currently selected <see cref="ItemContainer" />s.
/// </summary>
/// <remarks>This method has no effect if a dragging operation is already in progress.</remarks>
public void BeginDragging()
=> BeginDragging(SelectedContainers);
/// <summary>
/// Initiates the dragging operation for the specified <see cref="ItemContainer" />s. Call <see cref="EndDragging"/> to complete the operation or <see cref="CancelDragging"/> to abort it.
/// </summary>
/// <param name="containers">The collection of item containers to be dragged.</param>
/// <remarks>This method has no effect if a dragging operation is already in progress.</remarks>
public void BeginDragging(IEnumerable<ItemContainer> containers)
{
if (IsDragging)
{
return;
}
IsDragging = true;
_draggingStrategy = CreateDraggingStrategy(containers);
}
/// <summary>
/// Updates the position of the items being dragged by a specified offset.
/// </summary>
/// <param name="amount">The vector by which to adjust the position of the dragged items.</param>
/// <remarks>
/// This method adjusts the items positions incrementally. It should only be called while a dragging operation is in progress (see <see cref="BeginDragging" />).
/// </remarks>
public void UpdateDragging(Vector amount)
{
Debug.Assert(IsDragging);
_draggingStrategy!.Update(amount);
}
/// <summary>
/// Completes the dragging operation, finalizing the position of the dragged items. Raises the <see cref="ItemsMoved"/> event.
/// </summary>
/// <remarks>This method has no effect if there's no dragging operation in progress.</remarks>
public void EndDragging()
{
if (!IsDragging)
{
return;
}
var movedEvent = new ItemsMovedEventArgs(_draggingStrategy!.Containers.Select(x => x.DataContext).ToList(), _draggingStrategy.Offset)
{
RoutedEvent = ItemsMovedEvent,
Source = this
};
IsBulkUpdatingItems = true;
_draggingStrategy.End();
IsBulkUpdatingItems = false;
// Draw the containers at the new position.
ItemsHost.InvalidateArrange();
_draggingStrategy = null;
IsDragging = false;
RaiseEvent(movedEvent);
}
/// <summary>
/// Cancels the ongoing dragging operation, reverting any changes made to the positions of the dragged items if <see cref="AllowDraggingCancellation"/> is true.
/// Otherwise, it ends the dragging operation by calling <see cref="EndDragging"/>.
/// </summary>
/// <remarks>This method has no effect if there's no dragging operation in progress.</remarks>
public void CancelDragging()
{
if (!AllowDraggingCancellation)
{
EndDragging();
return;
}
if (IsDragging)
{
_draggingStrategy!.Abort();
IsDragging = false;
}
}
private IDraggingStrategy CreateDraggingStrategy(IEnumerable<ItemContainer> containers)
{
if (EnableDraggingContainersOptimizations)
{
return new DraggingOptimized(containers, GridCellSize);
}
return new DraggingSimple(containers, GridCellSize);
}
}
}

View File

@@ -0,0 +1,278 @@
using Nodify.Interactivity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using System.Windows;
using System.Collections;
using System.Diagnostics;
namespace Nodify
{
public partial class NodifyEditor : IKeyboardNavigationLayer, IKeyboardNavigationLayerGroup
{
/// <summary>
/// Gets or sets the default viewport edge offset applied when bringing an item into view as a result of keyboard focus.
/// </summary>
public static double BringIntoViewEdgeOffset { get; set; } = 32d;
/// <summary>
/// Automatically focus the first container when the navigation layer changes or the editor gets focused.
/// </summary>
public static bool AutoFocusFirstElement { get; set; } = true;
/// <summary>
/// Automatically pan the viewport when a node is focused via keyboard navigation.
/// </summary>
public static bool AutoPanOnNodeFocus { get; set; } = true;
/// <summary>
/// Automatically registers the decorators layer for keyboard navigation.
/// </summary>
public static bool AutoRegisterDecoratorsLayer { get; set; }
/// <summary>
/// Automatically registers the connectors layer for keyboard navigation.
/// </summary>
public static bool AutoRegisterConnectionsLayer { get; set; } = true;
/// <summary>
/// Indicates whether the viewport should automatically pan to follow elements moved via keyboard dragging.
/// </summary>
public static bool PanViewportOnKeyboardDrag { get; set; } = true;
/// <summary>
/// Defines the minimum distance to move or navigate when using directional input (such as arrow keys), scaled by the <see cref="ViewportZoom"/>.
/// If the <see cref="GridCellSize"/> is smaller than this value, the movement step is increased to the nearest greater multiple of the <see cref="GridCellSize"/>.
/// </summary>
public static double MinimumNavigationStepSize { get; set; } = 10d;
public IKeyboardNavigationLayer? ActiveNavigationLayer => _activeKeyboardNavigationLayer;
public IKeyboardNavigationLayer KeyboardNavigationLayer => this;
KeyboardNavigationLayerId IKeyboardNavigationLayer.Id => KeyboardNavigationLayerId.Nodes;
IKeyboardFocusTarget<UIElement>? IKeyboardNavigationLayer.LastFocusedElement => _focusNavigator.LastFocusedElement;
int IReadOnlyCollection<IKeyboardNavigationLayer>.Count => _navigationLayers.Count;
private readonly List<IKeyboardNavigationLayer> _navigationLayers = new List<IKeyboardNavigationLayer>();
private IKeyboardNavigationLayer? _activeKeyboardNavigationLayer;
#region Focus Handling
private readonly StatefulFocusNavigator<ItemContainer> _focusNavigator;
public event Action<KeyboardNavigationLayerId>? ActiveNavigationLayerChanged;
bool IKeyboardNavigationLayer.TryMoveFocus(TraversalRequest request)
{
return _focusNavigator.TryMoveFocus(request, TryFindContainerToFocus);
}
bool IKeyboardNavigationLayer.TryRestoreFocus()
{
return _focusNavigator.TryRestoreFocus();
}
private bool TryFindContainerToFocus(ItemContainer? currentElement, TraversalRequest request, out ItemContainer? containerToFocus)
{
containerToFocus = null;
if (currentElement is ItemContainer focusedContainer)
{
containerToFocus = FindNextFocusTarget(focusedContainer, request);
}
// The current element is not a nested editor, but a focusable element inside an ItemContainer
else if (currentElement is UIElement elem && elem != this && elem.GetParentOfType<ItemContainer>() is ItemContainer parentContainer)
{
containerToFocus = parentContainer;
}
else if (Items.Count > 0)
{
var viewport = new Rect(ViewportLocation, ViewportSize);
var containers = ItemContainers;
containerToFocus = containers.FirstOrDefault(container => container.IsSelectableInArea(viewport, isContained: false))
?? containers.First();
}
return containerToFocus != null;
}
protected virtual ItemContainer? FindNextFocusTarget(ItemContainer currentContainer, TraversalRequest request)
{
var focusNavigator = new DirectionalFocusNavigator<ItemContainer>(ItemContainers);
var result = focusNavigator.FindNextFocusTarget(currentContainer, request);
return result?.Element;
}
protected virtual void OnElementFocused(IKeyboardFocusTarget<ItemContainer> target)
{
if (AutoPanOnNodeFocus)
{
BringIntoView(target.Bounds, BringIntoViewEdgeOffset);
}
}
public bool MoveFocus(FocusNavigationDirection direction)
=> MoveFocus(new TraversalRequest(direction));
public new bool MoveFocus(TraversalRequest request)
=> ActiveNavigationLayer?.TryMoveFocus(request) ?? false;
void IKeyboardNavigationLayer.OnActivated()
{
KeyboardNavigationLayer.TryRestoreFocus();
}
void IKeyboardNavigationLayer.OnDeactivated()
{
}
protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
bool isKeyboardInitiated = InputManager.Current.MostRecentInputDevice is KeyboardDevice;
// When any focusable elements inside the editor - that are most likely inside containers (textbox, checkbox etc) - lose focus,
// and the focus goes outside the editor, we must focus its container first, otherwise focus the editor (don't allow focus to escape)
if (isKeyboardInitiated && e.OldFocus is DependencyObject oldFocus && !IsNavigationTrigger(oldFocus) && IsAncestorOf(oldFocus) && (e.NewFocus is DependencyObject newFocus && !IsAncestorOf(newFocus)))
{
var container = oldFocus.GetParent(IsNavigationTrigger);
if (container is UIElement elem && elem.Focus())
{
e.Handled = true;
}
else
{
e.Handled = Focus();
}
}
}
protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
bool isKeyboardInitiated = InputManager.Current.MostRecentInputDevice is KeyboardDevice;
if (isKeyboardInitiated && ActiveNavigationLayer != null)
{
bool isFocusComingFromOutside = e.OldFocus is null || e.OldFocus is DependencyObject dpo && !IsAncestorOf(dpo);
if (isFocusComingFromOutside && ActiveNavigationLayer.TryRestoreFocus())
{
e.Handled = true;
}
else if (ActiveNavigationLayer.LastFocusedElement is null && e.NewFocus == this && AutoFocusFirstElement)
{
e.Handled = ActiveNavigationLayer.TryMoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}
}
}
protected internal virtual bool IsNavigationTrigger(DependencyObject? dp)
{
return dp is NodifyEditor || dp is ItemContainer || dp is ConnectionContainer || dp is DecoratorContainer;
}
#endregion
#region Layer Management
protected virtual void OnKeyboardNavigationLayerActivated(IKeyboardNavigationLayer activeLayer)
{
if (AutoFocusFirstElement && !activeLayer!.TryRestoreFocus() && HandleNestedEditor())
{
activeLayer.TryMoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}
bool HandleNestedEditor()
{
var parentEditor = this.GetParentOfType<NodifyEditor>();
return parentEditor is null || parentEditor.IsKeyboardFocusWithin;
}
}
public bool RegisterNavigationLayer(IKeyboardNavigationLayer layer)
{
if (_navigationLayers.Any(l => l.Id == layer.Id))
{
return false;
}
_navigationLayers.Add(layer);
Debug.WriteLine($"Registered {layer} as a keyboard navigation layer in {this}");
return true;
}
public bool RemoveNavigationLayer(KeyboardNavigationLayerId layerId)
{
var layerToRemove = _navigationLayers.FirstOrDefault(layer => layer.Id == layerId);
if (layerToRemove != null && _navigationLayers.Remove(layerToRemove))
{
ActivatePreviousNavigationLayer();
return true;
}
return false;
}
public bool ActivateNavigationLayer(KeyboardNavigationLayerId layerId)
{
var newLayer = _navigationLayers.FirstOrDefault(x => x.Id == layerId);
if (newLayer != null)
{
var prevLayer = _activeKeyboardNavigationLayer;
_activeKeyboardNavigationLayer = newLayer;
prevLayer?.OnDeactivated();
newLayer.OnActivated();
OnKeyboardNavigationLayerActivated(newLayer);
Debug.WriteLine($"Activated {_activeKeyboardNavigationLayer} as a keyboard navigation layer in {this}");
ActiveNavigationLayerChanged?.Invoke(layerId);
return true;
}
return false;
}
public bool ActivateNextNavigationLayer()
{
if (_navigationLayers.Count > 0)
{
Debug.Assert(ActiveNavigationLayer != null);
int currentIndex = _navigationLayers.IndexOf(ActiveNavigationLayer!);
int nextIndex = (currentIndex + 1) % _navigationLayers.Count;
var layer = _navigationLayers[nextIndex];
return ActivateNavigationLayer(layer.Id);
}
return false;
}
public bool ActivatePreviousNavigationLayer()
{
if (_navigationLayers.Count > 0)
{
Debug.Assert(ActiveNavigationLayer != null);
int currentIndex = _navigationLayers.IndexOf(ActiveNavigationLayer!);
int prevIndex = (currentIndex - 1 + _navigationLayers.Count) % _navigationLayers.Count;
var layer = _navigationLayers[prevIndex];
return ActivateNavigationLayer(layer.Id);
}
return false;
}
public IEnumerator<IKeyboardNavigationLayer> GetEnumerator()
=> _navigationLayers.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
#endregion
}
}

View File

@@ -0,0 +1,230 @@
using Nodify.Interactivity;
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
namespace Nodify
{
public partial class NodifyEditor
{
#region Dependency properties
public static readonly DependencyProperty AutoPanSpeedProperty = DependencyProperty.Register(nameof(AutoPanSpeed), typeof(double), typeof(NodifyEditor), new FrameworkPropertyMetadata(15d));
public static readonly DependencyProperty AutoPanEdgeDistanceProperty = DependencyProperty.Register(nameof(AutoPanEdgeDistance), typeof(double), typeof(NodifyEditor), new FrameworkPropertyMetadata(15d));
public static readonly DependencyProperty DisableAutoPanningProperty = DependencyProperty.Register(nameof(DisableAutoPanning), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False, OnDisableAutoPanningChanged));
public static readonly DependencyProperty DisablePanningProperty = DependencyProperty.Register(nameof(DisablePanning), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False, OnDisablePanningChanged));
protected static readonly DependencyPropertyKey IsPanningPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsPanning), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False));
public static readonly DependencyProperty IsPanningProperty = IsPanningPropertyKey.DependencyProperty;
private static void OnDisableAutoPanningChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> ((NodifyEditor)d).OnDisableAutoPanningChanged((bool)e.NewValue);
private static void OnDisablePanningChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var editor = (NodifyEditor)d;
editor.OnDisableAutoPanningChanged(editor.DisableAutoPanning || editor.DisablePanning);
}
/// <summary>
/// Gets or sets whether panning should be disabled.
/// </summary>
public bool DisablePanning
{
get => (bool)GetValue(DisablePanningProperty);
set => SetValue(DisablePanningProperty, value);
}
/// <summary>
/// Gets or sets whether to disable the auto panning when selecting or dragging near the edge of the editor configured by <see cref="AutoPanEdgeDistance"/>.
/// </summary>
public bool DisableAutoPanning
{
get => (bool)GetValue(DisableAutoPanningProperty);
set => SetValue(DisableAutoPanningProperty, value);
}
/// <summary>
/// Gets or sets the speed used when auto-panning scaled by <see cref="AutoPanningTickRate"/>
/// </summary>
public double AutoPanSpeed
{
get => (double)GetValue(AutoPanSpeedProperty);
set => SetValue(AutoPanSpeedProperty, value);
}
/// <summary>
/// Gets or sets the maximum distance in pixels from the edge of the editor that will trigger auto-panning.
/// </summary>
public double AutoPanEdgeDistance
{
get => (double)GetValue(AutoPanEdgeDistanceProperty);
set => SetValue(AutoPanEdgeDistanceProperty, value);
}
/// <summary>
/// Gets a value that indicates whether a panning operation is in progress.
/// </summary>
public bool IsPanning
{
get => (bool)GetValue(IsPanningProperty);
private set => SetValue(IsPanningPropertyKey, value);
}
#endregion
/// <summary>
/// Gets or sets whether panning cancellation is allowed (see <see cref="EditorGestures.NodifyEditorGestures.CancelAction"/>).
/// </summary>
public static bool AllowPanningCancellation { get; set; }
/// <summary>
/// Gets or sets how often the new <see cref="ViewportLocation"/> is calculated in milliseconds when <see cref="DisableAutoPanning"/> is false.
/// </summary>
public static double AutoPanningTickRate { get; set; } = 1;
private DispatcherTimer? _autoPanningTimer;
private Point _initialPanningLocation;
/// <summary>
/// Starts the panning operation from the specified location. Call <see cref="EndPanning"/> to end the panning operation.
/// </summary>
/// <remarks>This method has no effect if a panning operation is already in progress.</remarks>
/// <param name="location">The initial location where panning starts, in graph space coordinates.</param>
public void BeginPanning(Point location)
{
if (IsPanning)
{
return;
}
_initialPanningLocation = location;
ViewportLocation = location;
IsPanning = true;
}
/// <summary>
/// Starts the panning operation from the current <see cref="ViewportLocation" />.
/// </summary>
/// <remarks>This method has no effect if a panning operation is already in progress.</remarks>
public void BeginPanning()
=> BeginPanning(ViewportLocation);
/// <summary>
/// Pans the viewport by the specified amount.
/// </summary>
/// <param name="amount">The amount to pan the viewport.</param>
/// <remarks>
/// This method adjusts the current <see cref="ViewportLocation"/> incrementally based on the provided amount.
/// </remarks>
public void UpdatePanning(Vector amount)
{
ViewportLocation -= amount;
}
/// <summary>
/// Ends the current panning operation, retaining the current <see cref="ViewportLocation"/>.
/// </summary>
/// <remarks>This method has no effect if there's no panning operation in progress.</remarks>
public void EndPanning()
{
IsPanning = false;
}
/// <summary>
/// Cancels the current panning operation and reverts the viewport to its initial location if <see cref="AllowPanningCancellation"/> is true.
/// Otherwise, it ends the panning operation by calling <see cref="EndPanning"/>.
/// </summary>
/// <remarks>This method has no effect if there's no panning operation in progress.</remarks>
public void CancelPanning()
{
if (!AllowPanningCancellation)
{
EndPanning();
return;
}
if (IsPanning)
{
ViewportLocation = _initialPanningLocation;
IsPanning = false;
}
}
#region Auto panning
private readonly MouseEventArgs _autoPanningEventArgs = new MouseEventArgs(Mouse.PrimaryDevice, 0, Stylus.CurrentStylusDevice)
{
RoutedEvent = MouseMoveEvent
};
private void HandleAutoPanning(object? sender, EventArgs e)
{
if (!IsPanning && IsMouseCaptureWithin)
{
Point mousePosition = Mouse.GetPosition(this);
double edgeDistance = AutoPanEdgeDistance;
double autoPanSpeed = Math.Min(AutoPanSpeed, AutoPanSpeed * AutoPanningTickRate) / (ViewportZoom * 2);
double x = ViewportLocation.X;
double y = ViewportLocation.Y;
if (mousePosition.X <= edgeDistance)
{
x -= autoPanSpeed;
}
else if (mousePosition.X >= ActualWidth - edgeDistance)
{
x += autoPanSpeed;
}
if (mousePosition.Y <= edgeDistance)
{
y -= autoPanSpeed;
}
else if (mousePosition.Y >= ActualHeight - edgeDistance)
{
y += autoPanSpeed;
}
ViewportLocation = new Point(x, y);
MouseLocation = Mouse.GetPosition(ItemsHost);
_autoPanningEventArgs.Handled = false;
_autoPanningEventArgs.Source = this;
InputProcessor.ProcessEvent(_autoPanningEventArgs);
}
}
/// <summary>
/// Called when the <see cref="DisableAutoPanning"/> changes.
/// </summary>
/// <param name="shouldDisable">Whether to enable or disable auto panning.</param>
private void OnDisableAutoPanningChanged(bool shouldDisable)
{
ClearTimer();
if (!shouldDisable)
{
_autoPanningTimer = new DispatcherTimer(DispatcherPriority.Background, Dispatcher)
{
Interval = TimeSpan.FromMilliseconds(AutoPanningTickRate)
};
_autoPanningTimer.Tick += HandleAutoPanning;
_autoPanningTimer.Start();
}
void ClearTimer()
{
if (_autoPanningTimer != null)
{
_autoPanningTimer.Stop();
_autoPanningTimer.Tick -= HandleAutoPanning;
_autoPanningTimer = null;
}
}
}
#endregion
}
}

View File

@@ -0,0 +1,159 @@
using Nodify.Interactivity;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shapes;
namespace Nodify
{
[StyleTypedProperty(Property = nameof(PushedAreaStyle), StyleTargetType = typeof(Rectangle))]
public partial class NodifyEditor
{
#region Dependency properties
public static readonly DependencyProperty PushedAreaStyleProperty = DependencyProperty.Register(nameof(PushedAreaStyle), typeof(Style), typeof(NodifyEditor));
protected static readonly DependencyPropertyKey PushedAreaPropertyKey = DependencyProperty.RegisterReadOnly(nameof(PushedArea), typeof(Rect), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.Rect));
public static readonly DependencyProperty PushedAreaProperty = PushedAreaPropertyKey.DependencyProperty;
protected static readonly DependencyPropertyKey IsPushingItemsPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsPushingItems), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False));
public static readonly DependencyProperty IsPushingItemsProperty = IsPushingItemsPropertyKey.DependencyProperty;
protected static readonly DependencyPropertyKey PushedAreaOrientationPropertyKey = DependencyProperty.RegisterReadOnly(nameof(PushedAreaOrientation), typeof(Orientation), typeof(NodifyEditor), new FrameworkPropertyMetadata(Orientation.Horizontal));
public static readonly DependencyProperty PushedAreaOrientationProperty = PushedAreaOrientationPropertyKey.DependencyProperty;
/// <summary>
/// Gets the currently pushed area while <see cref="IsPushingItems"/> is true.
/// </summary>
public Rect PushedArea
{
get => (Rect)GetValue(PushedAreaProperty);
private set => SetValue(PushedAreaPropertyKey, value);
}
/// <summary>
/// Gets a value that indicates whether a pushing operation is in progress.
/// </summary>
public bool IsPushingItems
{
get => (bool)GetValue(IsPushingItemsProperty);
private set => SetValue(IsPushingItemsPropertyKey, value);
}
/// <summary>
/// Gets the orientation of the <see cref="PushedArea"/>.
/// </summary>
public Orientation PushedAreaOrientation
{
get => (Orientation)GetValue(PushedAreaOrientationProperty);
private set => SetValue(PushedAreaOrientationPropertyKey, value);
}
/// <summary>
/// Gets or sets the style to use for the pushed area.
/// </summary>
public Style PushedAreaStyle
{
get => (Style)GetValue(PushedAreaStyleProperty);
set => SetValue(PushedAreaStyleProperty, value);
}
#endregion
/// <summary>
/// Gets or sets whether push items cancellation is allowed (see <see cref="EditorGestures.NodifyEditorGestures.CancelAction"/>).
/// </summary>
/// <remarks>Has no effect if <see cref="AllowDraggingCancellation"/> is false.</remarks>
public static bool AllowPushItemsCancellation { get; set; } = true;
private IPushStrategy? _pushStrategy;
/// <summary>
/// Starts the pushing items operation at the specified location with the specified orientation.
/// </summary>
/// <remarks>This method has no effect if a pushing operation is already in progress.</remarks>
/// <param name="location">The starting location for pushing items, in graph space coordinates.</param>
/// <param name="orientation">The orientation of the <see cref="PushedArea"/>.</param>
public void BeginPushingItems(Point location, Orientation orientation)
{
if (IsPushingItems)
{
return;
}
IsPushingItems = true;
PushedAreaOrientation = orientation;
_pushStrategy = CreatePushStrategy(orientation);
PushedArea = _pushStrategy.Start(location);
}
/// <summary>
/// Updates the pushed area based on the specified amount taking the <see cref="PushedAreaOrientation"/> into account.
/// </summary>
/// <param name="amount">The amount to adjust the pushed area by.</param>
/// <remarks>
/// This method adjusts the pushed area incrementally. It should only be called while a pushing operation is in progress (see <see cref="BeginPushingItems(Point, Orientation)"/>).
/// </remarks>
public void UpdatePushedArea(Vector amount)
{
Debug.Assert(IsPushingItems);
PushedArea = _pushStrategy!.Push(amount);
}
/// <summary>
/// Ends the current pushing operation and finalizes the pushed area state.
/// </summary>
/// <remarks>This method has no effect if there's no pushing operation in progress.</remarks>
public void EndPushingItems()
{
if (!IsPushingItems)
{
return;
}
PushedArea = _pushStrategy!.End();
_pushStrategy = null;
IsPushingItems = false;
}
/// <summary>
/// Cancels the current pushing operation and reverts the <see cref="PushedArea"/> to its initial state if <see cref="AllowPushItemsCancellation"/> is true.
/// Otherwise, it ends the pushing operation by calling <see cref="EndPushingItems"/>.
/// </summary>
/// <remarks>This method has no effect if there's no pushing operation in progress.</remarks>
public void CancelPushingItems()
{
if (!AllowPushItemsCancellation)
{
EndPushingItems();
return;
}
if (IsPushingItems)
{
PushedArea = _pushStrategy!.Cancel();
IsPushingItems = false;
}
}
private void UpdatePushedArea()
{
if (IsPushingItems)
{
PushedArea = _pushStrategy!.GetPushedArea();
}
}
private IPushStrategy CreatePushStrategy(Orientation orientation)
{
if (orientation == Orientation.Horizontal)
{
return new HorizontalPushStrategy(this);
}
return new VerticalPushStrategy(this);
}
}
}

View File

@@ -0,0 +1,137 @@
using System.Windows.Controls;
using System.Windows;
using System;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Input;
namespace Nodify
{
public partial class NodifyEditor : IScrollInfo
{
/// <summary>
/// The number of units the mouse wheel is rotated to scroll one line.
/// </summary>
public static double ScrollIncrement { get; set; } = Mouse.MouseWheelDeltaForOneLine / 2;
bool IScrollInfo.CanHorizontallyScroll { get; set; }
bool IScrollInfo.CanVerticallyScroll { get; set; }
private double _extentWidth;
double IScrollInfo.ExtentWidth => _extentWidth;
private double _extentHeight;
double IScrollInfo.ExtentHeight => _extentHeight;
private double _horizontalOffset;
double IScrollInfo.HorizontalOffset => _horizontalOffset;
private double _verticalOffset;
double IScrollInfo.VerticalOffset => _verticalOffset;
double IScrollInfo.ViewportWidth => ViewportSize.Width;
double IScrollInfo.ViewportHeight => ViewportSize.Height;
ScrollViewer? IScrollInfo.ScrollOwner { get; set; }
private IScrollInfo ScrollInfo => this;
private Point? _viewportLocationBeforeScrolling;
private bool _isScrolling;
void IScrollInfo.LineUp()
=> ViewportLocation -= new Vector(0, ScrollIncrement / ViewportZoom);
void IScrollInfo.LineDown()
=> ViewportLocation += new Vector(0, ScrollIncrement / ViewportZoom);
void IScrollInfo.LineLeft()
=> ViewportLocation -= new Vector(ScrollIncrement / ViewportZoom, 0);
void IScrollInfo.LineRight()
=> ViewportLocation += new Vector(ScrollIncrement / ViewportZoom, 0);
void IScrollInfo.MouseWheelUp() => ScrollInfo.LineUp();
void IScrollInfo.MouseWheelDown() => ScrollInfo.LineDown();
void IScrollInfo.MouseWheelLeft() => ScrollInfo.LineLeft();
void IScrollInfo.MouseWheelRight() => ScrollInfo.LineRight();
void IScrollInfo.PageUp()
=> ViewportLocation = new Point(ViewportLocation.X, ViewportLocation.Y - ViewportSize.Height);
void IScrollInfo.PageDown()
=> ViewportLocation = new Point(ViewportLocation.X, ViewportLocation.Y + ViewportSize.Height);
void IScrollInfo.PageLeft()
=> ViewportLocation = new Point(ViewportLocation.X - ViewportSize.Width, ViewportLocation.Y);
void IScrollInfo.PageRight()
=> ViewportLocation = new Point(ViewportLocation.X + ViewportSize.Width, ViewportLocation.Y);
Rect IScrollInfo.MakeVisible(Visual visual, Rect rectangle)
{
// This is called when clicking on an item container. Uncomment to automatically scroll to the selected item container.
//if (visual is ItemContainer container)
//{
// var containerBounds = new Rect(container.Location, container.RenderSize);
// if (!new Rect(ViewportLocation, ViewportSize).Contains(containerBounds))
// {
// BringIntoView(containerBounds);
// return containerBounds;
// }
//}
return rectangle;
}
void IScrollInfo.SetHorizontalOffset(double offset)
{
_horizontalOffset = double.IsInfinity(offset) ? 0d : offset;
UpdateViewportLocationOnScroll();
}
void IScrollInfo.SetVerticalOffset(double offset)
{
_verticalOffset = double.IsInfinity(offset) ? 0d : offset;
UpdateViewportLocationOnScroll();
}
private void UpdateViewportLocationOnScroll()
{
if (!_viewportLocationBeforeScrolling.HasValue)
{
_viewportLocationBeforeScrolling = ViewportLocation;
}
_isScrolling = true;
double locationX = Math.Min(ItemsExtent.Left, _viewportLocationBeforeScrolling.Value.X) + ScrollInfo.HorizontalOffset;
double locationY = Math.Min(ItemsExtent.Top, _viewportLocationBeforeScrolling.Value.Y) + ScrollInfo.VerticalOffset;
ViewportLocation = new Point(locationX, locationY);
ScrollInfo.ScrollOwner?.InvalidateScrollInfo();
_isScrolling = false;
}
private void UpdateScrollbars()
{
// setting the ViewportLocation when manually scrolling triggers the ViewportUpdatedEvent which in turn calls this method, hence the !_isScrolling check
if (ScrollInfo.ScrollOwner != null && !_isScrolling)
{
_viewportLocationBeforeScrolling = null;
var extent = ItemsExtent;
extent.Union(new Rect(ViewportLocation, ViewportSize));
_extentHeight = extent.Height;
_extentWidth = extent.Width;
var scrollOffset = ViewportLocation - ItemsExtent.Location;
_horizontalOffset = Math.Max(0, scrollOffset.X);
_verticalOffset = Math.Max(0, scrollOffset.Y);
ScrollInfo.ScrollOwner.InvalidateScrollInfo();
}
}
}
}

View File

@@ -0,0 +1,546 @@
using System.Diagnostics;
using System.Windows.Controls.Primitives;
using System.Windows.Controls;
using System.Windows;
using System.Collections;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Windows.Input;
using System.Windows.Shapes;
using Nodify.Interactivity;
namespace Nodify
{
/// <summary>Available selection logic.</summary>
public enum SelectionType
{
/// <summary>Replaces the old selection.</summary>
Replace,
/// <summary>Removes items from existing selection.</summary>
Remove,
/// <summary>Adds items to the current selection.</summary>
Append,
/// <summary>Inverts the selection.</summary>
Invert
}
[StyleTypedProperty(Property = nameof(SelectionRectangleStyle), StyleTargetType = typeof(Rectangle))]
public partial class NodifyEditor : MultiSelector
{
#region Dependency properties
public static readonly DependencyProperty ItemsSelectStartedCommandProperty = DependencyProperty.Register(nameof(ItemsSelectStartedCommand), typeof(ICommand), typeof(NodifyEditor));
public static readonly DependencyProperty ItemsSelectCompletedCommandProperty = DependencyProperty.Register(nameof(ItemsSelectCompletedCommand), typeof(ICommand), typeof(NodifyEditor));
public static readonly DependencyProperty SelectionRectangleStyleProperty = DependencyProperty.Register(nameof(SelectionRectangleStyle), typeof(Style), typeof(NodifyEditor));
protected static readonly DependencyPropertyKey SelectedAreaPropertyKey = DependencyProperty.RegisterReadOnly(nameof(SelectedArea), typeof(Rect), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.Rect));
public static readonly DependencyProperty SelectedAreaProperty = SelectedAreaPropertyKey.DependencyProperty;
protected static readonly DependencyPropertyKey IsSelectingPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsSelecting), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False, OnIsSelectingChanged));
public static readonly DependencyProperty IsSelectingProperty = IsSelectingPropertyKey.DependencyProperty;
public static readonly DependencyProperty EnableRealtimeSelectionProperty = DependencyProperty.Register(nameof(EnableRealtimeSelection), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False));
public static readonly DependencyProperty CanSelectMultipleConnectionsProperty = DependencyProperty.Register(nameof(CanSelectMultipleConnections), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.True));
public static readonly DependencyProperty CanSelectMultipleItemsProperty = DependencyProperty.Register(nameof(CanSelectMultipleItems), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.True, OnCanSelectMultipleItemsChanged, CoerceCanSelectMultipleItems));
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(nameof(SelectedItems), typeof(IList), typeof(NodifyEditor), new FrameworkPropertyMetadata(default(IList), OnSelectedItemsSourceChanged));
public static readonly DependencyProperty SelectedConnectionsProperty = DependencyProperty.Register(nameof(SelectedConnections), typeof(IList), typeof(NodifyEditor), new FrameworkPropertyMetadata(default(IList)));
public static readonly DependencyProperty SelectedConnectionProperty = DependencyProperty.Register(nameof(SelectedConnection), typeof(object), typeof(NodifyEditor), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
private static void OnCanSelectMultipleItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> ((NodifyEditor)d).CanSelectMultipleItemsBase = (bool)e.NewValue;
private static object CoerceCanSelectMultipleItems(DependencyObject d, object baseValue)
=> ((NodifyEditor)d).CanSelectMultipleItemsBase = (bool)baseValue;
private static void OnSelectedItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> ((NodifyEditor)d).OnSelectedItemsSourceChanged((IList)e.OldValue, (IList)e.NewValue);
private static void OnIsSelectingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var editor = (NodifyEditor)d;
if ((bool)e.NewValue == true)
editor.OnItemsSelectStarted();
else
editor.OnItemsSelectCompleted();
}
private void OnItemsSelectCompleted()
{
if (ItemsSelectCompletedCommand?.CanExecute(DataContext) ?? false)
ItemsSelectCompletedCommand.Execute(DataContext);
}
private void OnItemsSelectStarted()
{
if (ItemsSelectStartedCommand?.CanExecute(DataContext) ?? false)
ItemsSelectStartedCommand.Execute(DataContext);
}
/// <summary>Invoked when a selection operation is started (see <see cref="BeginSelecting(SelectionType)"/>).</summary>
public ICommand? ItemsSelectStartedCommand
{
get => (ICommand?)GetValue(ItemsSelectStartedCommandProperty);
set => SetValue(ItemsSelectStartedCommandProperty, value);
}
/// <summary>Invoked when a selection operation is completed (see <see cref="EndSelecting"/>).</summary>
public ICommand? ItemsSelectCompletedCommand
{
get => (ICommand?)GetValue(ItemsSelectCompletedCommandProperty);
set => SetValue(ItemsSelectCompletedCommandProperty, value);
}
/// <summary>
/// Gets or sets whether multiple connections can be selected.
/// </summary>
public bool CanSelectMultipleConnections
{
get => (bool)GetValue(CanSelectMultipleConnectionsProperty);
set => SetValue(CanSelectMultipleConnectionsProperty, value);
}
/// <summary>
/// Gets or sets whether multiple <see cref="ItemContainer" />s can be selected.
/// </summary>
public new bool CanSelectMultipleItems
{
get => (bool)GetValue(CanSelectMultipleItemsProperty);
set => SetValue(CanSelectMultipleItemsProperty, value);
}
private bool CanSelectMultipleItemsBase
{
get => base.CanSelectMultipleItems;
set => base.CanSelectMultipleItems = value;
}
/// <summary>
/// Enables selecting and deselecting items while the <see cref="SelectedArea"/> changes.
/// Disable for maximum performance when hundreds of items are generated.
/// </summary>
public bool EnableRealtimeSelection
{
get => (bool)GetValue(EnableRealtimeSelectionProperty);
set => SetValue(EnableRealtimeSelectionProperty, value);
}
/// <summary>
/// Gets or sets the selected connection.
/// </summary>
public object? SelectedConnection
{
get => GetValue(SelectedConnectionProperty);
set => SetValue(SelectedConnectionProperty, value);
}
/// <summary>
/// Gets or sets the selected connections in the <see cref="NodifyEditor"/>.
/// </summary>
public IList? SelectedConnections
{
get => (IList?)GetValue(SelectedConnectionsProperty);
set => SetValue(SelectedConnectionsProperty, value);
}
/// <summary>
/// Gets or sets the selected items in the <see cref="NodifyEditor"/>.
/// </summary>
public new IList? SelectedItems
{
get => (IList?)GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
/// <summary>
/// Gets the currently selected area while <see cref="IsSelecting"/> is true.
/// </summary>
public Rect SelectedArea
{
get => (Rect)GetValue(SelectedAreaProperty);
private set => SetValue(SelectedAreaPropertyKey, value);
}
/// <summary>
/// Gets a value that indicates whether a selection operation is in progress.
/// </summary>
public bool IsSelecting
{
get => (bool)GetValue(IsSelectingProperty);
private set => SetValue(IsSelectingPropertyKey, value);
}
/// <summary>
/// Gets or sets the style to use for the selection rectangle.
/// </summary>
public Style SelectionRectangleStyle
{
get => (Style)GetValue(SelectionRectangleStyleProperty);
set => SetValue(SelectionRectangleStyleProperty, value);
}
#endregion
/// <summary>
/// Gets a list of <see cref="ItemContainer"/>s that are selected (see <see cref="SelectedContainersCount"/>).
/// </summary>
/// <remarks>Cache the result before using it to avoid extra allocations.</remarks>
protected internal IReadOnlyList<ItemContainer> SelectedContainers
{
get
{
IList selectedItems = base.SelectedItems;
var selectedContainers = new List<ItemContainer>(selectedItems.Count);
for (var i = 0; i < selectedItems.Count; i++)
{
var container = (ItemContainer)ItemContainerGenerator.ContainerFromItem(selectedItems[i]);
selectedContainers.Add(container);
}
return selectedContainers;
}
}
/// <summary>
/// Gets the number of selected containers, without allocating (see <see cref="SelectedContainers"/>).
/// </summary>
public int SelectedContainersCount => base.SelectedItems.Count;
/// <summary>
/// Gets or sets whether cancelling a selection operation is allowed (see <see cref="EditorGestures.SelectionGestures.Cancel"/>).
/// </summary>
public static bool AllowSelectionCancellation { get; set; } = true;
/// <summary>The selection helper.</summary>
private readonly SelectionHelper _selection = new SelectionHelper();
#region Selection
/// <summary>
/// Inverts the <see cref="ItemContainer"/>s selection in the specified <paramref name="area"/>.
/// </summary>
/// <param name="area">The area to look for <see cref="ItemContainer"/>s.</param>
/// <param name="fit">True to check if the <paramref name="area"/> contains the <see cref="ItemContainer"/>. <br />False to check if <paramref name="area"/> intersects the <see cref="ItemContainer"/>.</param>
public void InvertSelection(Rect area, bool fit = false)
{
ItemCollection items = Items;
IList selected = base.SelectedItems;
IsSelecting = true;
BeginUpdateSelectedItems();
for (var i = 0; i < items.Count; i++)
{
var container = (ItemContainer)ItemContainerGenerator.ContainerFromIndex(i);
if (container.IsSelectableInArea(area, fit))
{
object? item = items[i];
if (container.IsSelected)
{
selected.Remove(item);
}
else
{
selected.Add(item);
}
}
}
EndUpdateSelectedItems();
IsSelecting = false;
}
/// <summary>
/// Selects the <see cref="ItemContainer"/>s in the specified <paramref name="area"/>.
/// </summary>
/// <param name="area">The area to look for <see cref="ItemContainer"/>s.</param>
/// <param name="append">If true, it will add to the existing selection.</param>
/// <param name="fit">True to check if the <paramref name="area"/> contains the <see cref="ItemContainer"/>. <br />False to check if <paramref name="area"/> intersects the <see cref="ItemContainer"/>.</param>
public void SelectArea(Rect area, bool append = false, bool fit = false)
{
IsSelecting = true;
BeginUpdateSelectedItems();
IList selected = base.SelectedItems;
if (!append)
{
selected.Clear();
}
ItemCollection items = Items;
for (var i = 0; i < items.Count; i++)
{
var container = (ItemContainer)ItemContainerGenerator.ContainerFromIndex(i);
if (container.IsSelectableInArea(area, fit))
{
selected.Add(items[i]);
}
}
EndUpdateSelectedItems();
IsSelecting = false;
}
/// <summary>
/// Clears the current selection and selects the specified <see cref="ItemContainer"/> within the same selection transaction.
/// </summary>
/// <param name="container"></param>
public void Select(ItemContainer container)
{
BeginUpdateSelectedItems();
var selected = base.SelectedItems;
selected.Clear();
selected.Add(container.DataContext);
EndUpdateSelectedItems();
UnselectAllConnections();
}
/// <summary>
/// Unselect the <see cref="ItemContainer"/>s in the specified <paramref name="area"/>.
/// </summary>
/// <param name="area">The area to look for <see cref="ItemContainer"/>s.</param>
/// <param name="fit">True to check if the <paramref name="area"/> contains the <see cref="ItemContainer"/>. <br />False to check if <paramref name="area"/> intersects the <see cref="ItemContainer"/>.</param>
public void UnselectArea(Rect area, bool fit = false)
{
IList items = base.SelectedItems;
IsSelecting = true;
BeginUpdateSelectedItems();
for (var i = 0; i < items.Count; i++)
{
var container = (ItemContainer)ItemContainerGenerator.ContainerFromItem(items[i]);
if (container.IsSelectableInArea(area, fit))
{
items.Remove(items[i]);
}
}
EndUpdateSelectedItems();
IsSelecting = false;
}
/// <summary>
/// Unselect all <see cref="Connections"/>.
/// </summary>
public void UnselectAllConnections()
{
if (ConnectionsHost is MultiSelector selector)
{
selector.UnselectAll();
}
}
/// <summary>
/// Select all <see cref="Connections"/>.
/// </summary>
public void SelectAllConnections()
{
if (ConnectionsHost is MultiSelector selector)
{
selector.SelectAll();
}
}
/// <summary>
/// Initiates a selection operation from the current <see cref="MouseLocation"/>.
/// </summary>
/// <remarks>This method has no effect if a selection operation is already in progress.</remarks>
/// <param name="type">The type of selection to perform. Defaults to <see cref="SelectionType.Replace"/>.</param>
public void BeginSelecting(SelectionType type = SelectionType.Replace)
=> BeginSelecting(MouseLocation, type);
/// <summary>
/// Initiates a selection operation from the specified location.
/// </summary>
/// <remarks>This method has no effect if a selection operation is already in progress.</remarks>
/// <param name="location">The starting point for the selection, in graph space coordinates.</param>
/// <param name="type">The type of selection to perform. Defaults to <see cref="SelectionType.Replace"/>.</param>
public void BeginSelecting(Point location, SelectionType type = SelectionType.Replace)
{
if (IsSelecting)
{
return;
}
SelectedArea = _selection.Start(ItemContainers, location, type, EnableRealtimeSelection);
IsSelecting = true;
}
/// <summary>
/// Expands or modifies the selection area by the specified amount.
/// </summary>
/// <param name="amount">Rrepresents the change to apply to the selection area.</param>
public void UpdateSelection(Vector amount)
{
Debug.Assert(IsSelecting);
SelectedArea = _selection.Update(amount);
}
/// <summary>
/// Expands or modifies the selection area to the specified location.
/// </summary>
/// <param name="location">The point, in graph space coordinates, to extend or adjust the selection area to.</param>
public void UpdateSelection(Point location)
{
Debug.Assert(IsSelecting);
SelectedArea = _selection.Update(location);
}
/// <summary>
/// Completes the selection operation and applies any pending changes.
/// </summary>
/// <remarks>This method has no effect if there's no selection operation in progress.</remarks>
public void EndSelecting()
{
if (!IsSelecting)
{
return;
}
if (_selection.Type == SelectionType.Replace)
{
UnselectAllConnections();
}
SelectedArea = _selection.End();
IsSelecting = false;
ApplyPreviewingSelection();
}
/// <summary>
/// Cancels the current selection operation and reverts any changes made during the selection process if <see cref="AllowSelectionCancellation"/> is true.
/// Otherwise, it ends the selection operation by calling <see cref="EndSelecting"/>.
/// </summary>
/// <remarks>This method has no effect if there's no selection operation in progress.</remarks>
public void CancelSelecting()
{
if (!AllowSelectionCancellation)
{
EndSelecting();
return;
}
if (IsSelecting)
{
_selection.Cancel();
IsSelecting = false;
}
}
private void ApplyPreviewingSelection()
{
ItemCollection items = Items;
IList selected = base.SelectedItems;
BeginUpdateSelectedItems();
for (var i = 0; i < items.Count; i++)
{
var container = (ItemContainer)ItemContainerGenerator.ContainerFromIndex(i);
if (container.IsPreviewingSelection == true && container.IsSelectable)
{
selected.Add(items[i]);
}
else if (container.IsPreviewingSelection == false)
{
selected.Remove(items[i]);
}
container.IsPreviewingSelection = null;
}
EndUpdateSelectedItems();
}
#endregion
#region Selection Handlers
private void OnSelectedItemsSourceChanged(IList oldValue, IList newValue)
{
if (oldValue is INotifyCollectionChanged oc)
{
oc.CollectionChanged -= OnSelectedItemsChanged;
}
if (newValue is INotifyCollectionChanged nc)
{
nc.CollectionChanged += OnSelectedItemsChanged;
}
IList selectedItems = base.SelectedItems;
BeginUpdateSelectedItems();
selectedItems.Clear();
if (newValue != null)
{
for (var i = 0; i < newValue.Count; i++)
{
selectedItems.Add(newValue[i]);
}
}
EndUpdateSelectedItems();
}
private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (!CanSelectMultipleItems)
return;
switch (e.Action)
{
case NotifyCollectionChangedAction.Reset:
base.SelectedItems.Clear();
break;
case NotifyCollectionChangedAction.Add:
IList? newItems = e.NewItems;
if (newItems != null)
{
IList selectedItems = base.SelectedItems;
for (var i = 0; i < newItems.Count; i++)
{
selectedItems.Add(newItems[i]);
}
}
break;
case NotifyCollectionChangedAction.Remove:
IList? oldItems = e.OldItems;
if (oldItems != null)
{
IList selectedItems = base.SelectedItems;
for (var i = 0; i < oldItems.Count; i++)
{
selectedItems.Remove(oldItems[i]);
}
}
break;
}
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
IList? selected = SelectedItems;
if (selected != null)
{
IList added = e.AddedItems;
for (var i = 0; i < added.Count; i++)
{
// Ensure no duplicates are added
if (!selected.Contains(added[i]))
{
selected.Add(added[i]);
}
}
IList removed = e.RemovedItems;
for (var i = 0; i < removed.Count; i++)
{
selected.Remove(removed[i]);
}
}
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
using System.Windows.Input;
namespace Nodify.Interactivity
{
public static partial class EditorState
{
/// <summary>
/// Represents the cutting state in the <see cref="NodifyEditor"/>, allowing users to cut connections between elements using a drag gesture.
/// </summary>
public class Cutting : DragState<NodifyEditor>
{
protected override bool HasContextMenu => Element.HasContextMenu;
protected override bool CanBegin => !Element.IsSelecting && !Element.IsPanning && !Element.IsPushingItems;
protected override bool CanCancel => NodifyEditor.AllowCuttingCancellation;
protected override bool IsToggle => EnableToggledCuttingMode;
/// <summary>
/// Initializes a new instance of the <see cref="Cutting"/> class.
/// </summary>
/// <param name="editor">The <see cref="NodifyEditor"/> associated with this state.</param>
public Cutting(NodifyEditor editor)
: base(editor, EditorGestures.Mappings.Editor.Cutting, EditorGestures.Mappings.Editor.CancelAction)
{
}
protected override void OnBegin(InputEventArgs e)
=> Element.BeginCutting();
protected override void OnMouseMove(MouseEventArgs e)
=> Element.UpdateCuttingLine(Element.MouseLocation);
protected override void OnEnd(InputEventArgs e)
=> Element.EndCutting();
protected override void OnCancel(InputEventArgs e)
=> Element.CancelCutting();
}
}
}

View File

@@ -0,0 +1,71 @@
namespace Nodify.Interactivity
{
public static partial class EditorState
{
/// <summary>
/// Gets or sets a value indicating whether panning is allowed while selecting items in the editor.
/// </summary>
public static bool AllowPanningWhileSelecting { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether panning is allowed while cutting connections in the editor.
/// </summary>
public static bool AllowPanningWhileCutting { get; set; }
/// <summary>
/// Gets or sets a value indicating whether panning is allowed while pushing items in the editor.
/// </summary>
public static bool AllowPanningWhilePushingItems { get; set; }
/// <summary>
/// Gets or sets a value indicating whether zooming is allowed while selecting items in the editor.
/// </summary>
public static bool AllowZoomingWhileSelecting { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether zooming is allowed while cutting connections in the editor.
/// </summary>
public static bool AllowZoomingWhileCutting { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether zooming is allowed while pushing items in the editor.
/// </summary>
public static bool AllowZoomingWhilePushingItems { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether zooming is allowed while panning the editor viewport.
/// </summary>
public static bool AllowZoomingWhilePanning { get; set; } = true;
/// <summary>
/// Determines whether toggled selecting mode is enabled, allowing the user to start and end the interaction in two steps with the same input gesture.
/// </summary>
public static bool EnableToggledSelectingMode { get; set; }
/// <summary>
/// Determines whether toggled panning mode is enabled, allowing the user to start and end the interaction in two steps with the same input gesture.
/// </summary>
public static bool EnableToggledPanningMode { get; set; }
/// <summary>
/// Determines whether toggled cutting mode is enabled, allowing the user to start and end the interaction in two steps with the same input gesture.
/// </summary>
public static bool EnableToggledCuttingMode { get; set; }
/// <summary>
/// Determines whether toggled pushing items mode is enabled, allowing the user to start and end the interaction in two steps with the same input gesture.
/// </summary>
public static bool EnableToggledPushingItemsMode { get; set; }
internal static void RegisterDefaultHandlers()
{
InputProcessor.Shared<NodifyEditor>.RegisterHandlerFactory(elem => new Panning(elem));
InputProcessor.Shared<NodifyEditor>.RegisterHandlerFactory(elem => new PanningWithMouseWheel(elem));
InputProcessor.Shared<NodifyEditor>.RegisterHandlerFactory(elem => new Selecting(elem));
InputProcessor.Shared<NodifyEditor>.RegisterHandlerFactory(elem => new Zooming(elem));
InputProcessor.Shared<NodifyEditor>.RegisterHandlerFactory(elem => new PushingItems(elem));
InputProcessor.Shared<NodifyEditor>.RegisterHandlerFactory(elem => new Cutting(elem));
InputProcessor.Shared<NodifyEditor>.RegisterHandlerFactory(elem => new KeyboardNavigation(elem));
}
}
}

View File

@@ -0,0 +1,147 @@
using System;
using System.Windows;
using System.Windows.Input;
namespace Nodify.Interactivity
{
public static partial class EditorState
{
/// <summary>
/// Represents the keyboard navigation state of the <see cref="NodifyEditor"/>, allowing users to navigate and interact with nodes and connections using the keyboard.
/// </summary>
public class KeyboardNavigation : InputElementState<NodifyEditor>
{
/// <summary>
/// Initializes a new instance of the <see cref="KeyboardNavigation"/> class.
/// </summary>
/// <param name="element">The <see cref="NodifyEditor"/> associated with this state.</param>
public KeyboardNavigation(NodifyEditor element) : base(element)
{
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (!Element.IsKeyboardFocusWithin || !(e.OriginalSource is DependencyObject originalSource))
{
return;
}
double navigationStepSize = GetNavigationStepSize();
var gestures = EditorGestures.Mappings.Editor.Keyboard;
if (e.Key == Key.Tab && Keyboard.Modifiers.HasFlag(ModifierKeys.Control))
{
var parentContainer = originalSource.GetParent(Element.IsNavigationTrigger) as UIElement;
e.Handled = parentContainer?.Focus() is true;
}
else if (Element.IsNavigationTrigger(originalSource))
{
if (gestures.Pan.TryGetNavigationDirection(e, out var panDirection))
{
var panning = new Vector(-panDirection.X * navigationStepSize, panDirection.Y * navigationStepSize);
Element.UpdatePanning(panning);
e.Handled = true;
}
else if (CanDragSelection() && gestures.DragSelection.TryGetNavigationDirection(e, out var dragDirection))
{
var dragging = new Vector(dragDirection.X * navigationStepSize, -dragDirection.Y * navigationStepSize);
Element.BeginDragging();
Element.UpdateDragging(dragging);
Element.EndDragging();
if (NodifyEditor.PanViewportOnKeyboardDrag)
{
var panning = new Vector(-dragDirection.X * navigationStepSize, dragDirection.Y * navigationStepSize);
Element.UpdatePanning(panning);
}
e.Handled = true;
}
else if (gestures.NavigateSelection.TryGetFocusDirection(e, out var direction))
{
Element.MoveFocus(direction);
e.Handled = true;
}
}
}
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);
if (NodifyEditor.AutoPanOnNodeFocus)
{
Element.BringIntoView(itemContainer.Bounds, NodifyEditor.BringIntoViewEdgeOffset);
}
}
else if (Keyboard.FocusedElement is ConnectionContainer connectionContainer)
{
connectionContainer.Select(SelectionType.Invert);
if (NodifyEditor.AutoPanOnNodeFocus)
{
Element.BringIntoView(connectionContainer.Bounds, NodifyEditor.BringIntoViewEdgeOffset);
}
}
e.Handled = true;
}
else if (gestures.DeselectAll.Matches(e.Source, e))
{
if (Element.SelectedContainersCount > 0 && Element.ActiveNavigationLayer?.Id == KeyboardNavigationLayerId.Nodes)
{
Element.UnselectAll();
e.Handled = true;
}
// TODO: How to get the selected connections count without a hard reference to the connections multi selector?
// This currently assumes we have a binding to the SelectedConnectionsProperty dependency property
else if (Element.SelectedConnections?.Count > 0 && Element.ActiveNavigationLayer?.Id == KeyboardNavigationLayerId.Connections)
{
Element.UnselectAllConnections();
e.Handled = true;
}
}
else if (gestures.NextNavigationLayer.Matches(e.Source, e))
{
Element.ActivateNextNavigationLayer();
e.Handled = true;
}
else if (gestures.PrevNavigationLayer.Matches(e.Source, e))
{
Element.ActivatePreviousNavigationLayer();
e.Handled = true;
}
else if (Keyboard.FocusedElement is ItemContainer { IsSelected: true } container
&& EditorGestures.Mappings.GroupingNode.ToggleContentSelection.Matches(e.Source, e))
{
var groupingNode = container.GetChildOfType<GroupingNode>();
if (groupingNode != null)
{
groupingNode.ToggleContentSelection();
e.Handled = true;
}
}
}
private bool CanDragSelection()
{
return Element.ActiveNavigationLayer?.Id == KeyboardNavigationLayerId.Nodes && Element.SelectedContainersCount > 0;
}
private double GetNavigationStepSize()
{
double cellSize = Element.GridCellSize;
if (cellSize >= NodifyEditor.MinimumNavigationStepSize)
return cellSize;
int factor = (int)Math.Ceiling(NodifyEditor.MinimumNavigationStepSize / cellSize);
return factor * cellSize / Element.ViewportZoom;
}
}
}
}

View File

@@ -0,0 +1,91 @@
using System;
using System.Windows;
using System.Windows.Input;
namespace Nodify.Interactivity
{
public static partial class EditorState
{
/// <summary>
/// Represents the panning state of the <see cref="NodifyEditor"/>, allowing the user to pan the viewport by clicking and dragging.
/// </summary>
public class Panning : DragState<NodifyEditor>
{
protected override bool HasContextMenu => Element.HasContextMenu;
protected override bool CanBegin => IsPanningAllowed();
protected override bool CanCancel => NodifyEditor.AllowPanningCancellation;
protected override bool IsToggle => EnableToggledPanningMode;
private Point _prevPosition;
/// <summary>
/// Initializes a new instance of the <see cref="Panning"/> class.
/// </summary>
/// <param name="editor">The <see cref="NodifyEditor"/> associated with this state.</param>
public Panning(NodifyEditor editor)
: base(editor, EditorGestures.Mappings.Editor.Pan, EditorGestures.Mappings.Editor.CancelAction)
{
}
protected override void OnBegin(InputEventArgs e)
{
_prevPosition = Mouse.GetPosition(Element);
Element.BeginPanning();
}
protected override void OnMouseMove(MouseEventArgs e)
{
var currentMousePosition = e.GetPosition(Element);
Element.UpdatePanning((currentMousePosition - _prevPosition) / Element.ViewportZoom);
_prevPosition = currentMousePosition;
}
protected override void OnEnd(InputEventArgs e)
=> Element.EndPanning();
protected override void OnCancel(InputEventArgs e)
=> Element.CancelPanning();
private bool IsPanningAllowed()
{
return !Element.DisablePanning
&& (AllowPanningWhileSelecting || !Element.IsSelecting)
&& (AllowPanningWhileCutting || !Element.IsCutting)
&& (AllowPanningWhilePushingItems || !Element.IsPushingItems);
}
}
/// <summary>
/// Represents the panning state of the <see cref="NodifyEditor"/> using the mouse wheel.
/// Allows the user to pan horizontally or vertically by holding modifier keys while scrolling the mouse wheel.
/// </summary>
public class PanningWithMouseWheel : InputElementState<NodifyEditor>
{
/// <summary>
/// Initializes a new instance of the <see cref="PanningWithMouseWheel"/> class.
/// </summary>
/// <param name="editor">The <see cref="NodifyEditor"/> associated with this state.</param>
public PanningWithMouseWheel(NodifyEditor editor) : base(editor)
{
}
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
EditorGestures.NodifyEditorGestures gestures = EditorGestures.Mappings.Editor;
if (gestures.PanWithMouseWheel && Keyboard.Modifiers == gestures.PanHorizontalModifierKey)
{
double offset = Math.Sign(e.Delta) * Mouse.MouseWheelDeltaForOneLine / 2 / Element.ViewportZoom;
Element.UpdatePanning(new Vector(offset, 0d));
e.Handled = true;
}
else if (gestures.PanWithMouseWheel && Keyboard.Modifiers == gestures.PanVerticalModifierKey)
{
double offset = Math.Sign(e.Delta) * Mouse.MouseWheelDeltaForOneLine / 2 / Element.ViewportZoom;
Element.UpdatePanning(new Vector(0d, offset));
e.Handled = true;
}
}
}
}
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Nodify.Interactivity
{
public static partial class EditorState
{
/// <summary>
/// Represents the state of the <see cref="NodifyEditor"/> during a "push items" operation, allowing users to move items within the editor by dragging.
/// </summary>
public class PushingItems : DragState<NodifyEditor>
{
protected override bool HasContextMenu => Element.HasContextMenu;
protected override bool CanBegin => !Element.IsSelecting && !Element.IsPanning && !Element.IsCutting;
protected override bool CanCancel => NodifyEditor.AllowPushItemsCancellation;
protected override bool IsToggle => EnableToggledPushingItemsMode;
private Point _prevPosition;
/// <summary>
/// Initializes a new instance of the <see cref="PushingItems"/> class.
/// </summary>
/// <param name="editor">The <see cref="NodifyEditor"/> associated with this state.</param>
public PushingItems(NodifyEditor editor)
: base(editor, EditorGestures.Mappings.Editor.PushItems, EditorGestures.Mappings.Editor.CancelAction)
{
}
protected override void OnBegin(InputEventArgs e)
=> _prevPosition = Element.MouseLocation;
protected override void OnMouseMove(MouseEventArgs e)
{
if (Element.IsPushingItems)
{
Element.UpdatePushedArea(Element.MouseLocation - _prevPosition);
_prevPosition = Element.MouseLocation;
}
else
{
if (Math.Abs(Element.MouseLocation.X - _prevPosition.X) >= NodifyEditor.MouseActionSuppressionThreshold)
{
Element.BeginPushingItems(_prevPosition, Orientation.Horizontal);
}
else if (Math.Abs(Element.MouseLocation.Y - _prevPosition.Y) >= NodifyEditor.MouseActionSuppressionThreshold)
{
Element.BeginPushingItems(_prevPosition, Orientation.Vertical);
}
}
}
protected override void OnEnd(InputEventArgs e)
=> Element.EndPushingItems();
protected override void OnCancel(InputEventArgs e)
=> Element.CancelPushingItems();
}
}
}

View File

@@ -0,0 +1,43 @@
using System.Windows.Input;
namespace Nodify.Interactivity
{
public static partial class EditorState
{
/// <summary>
/// Represents the selecting state of the <see cref="NodifyEditor"/>.
/// This state is responsible for handling item selection within the editor.
/// </summary>
public class Selecting : DragState<NodifyEditor>
{
protected override bool HasContextMenu => Element.HasContextMenu;
protected override bool CanBegin => Element.CanSelectMultipleItems && !Element.IsPanning && !Element.IsCutting && !Element.IsPushingItems;
protected override bool CanCancel => NodifyEditor.AllowSelectionCancellation;
protected override bool IsToggle => EnableToggledSelectingMode;
/// <summary>
/// Initializes a new instance of the <see cref="Selecting"/> class.
/// </summary>
/// <param name="editor">The <see cref="NodifyEditor"/> associated with this state.</param>
public Selecting(NodifyEditor editor)
: base(editor, EditorGestures.Mappings.Editor.Selection.Select, EditorGestures.Mappings.Editor.Selection.Cancel)
{
}
protected override void OnBegin(InputEventArgs e)
{
var selectionType = EditorGestures.Mappings.Editor.Selection.GetSelectionType(e);
Element.BeginSelecting(selectionType);
}
protected override void OnMouseMove(MouseEventArgs e)
=> Element.UpdateSelection(Element.MouseLocation);
protected override void OnEnd(InputEventArgs e)
=> Element.EndSelecting();
protected override void OnCancel(InputEventArgs e)
=> Element.CancelSelecting();
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Windows.Input;
namespace Nodify.Interactivity
{
public static partial class EditorState
{
/// <summary>
/// Represents the zooming state of the <see cref="NodifyEditor"/>.
/// This state handles zooming operations using the mouse wheel with an optional modifier key.
/// </summary>
public class Zooming : InputElementState<NodifyEditor>
{
/// <summary>
/// Initializes a new instance of the <see cref="Zooming"/> class.
/// </summary>
/// <param name="editor">The <see cref="NodifyEditor"/> associated with this state.</param>
public Zooming(NodifyEditor editor) : base(editor)
{
}
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
EditorGestures.NodifyEditorGestures gestures = EditorGestures.Mappings.Editor;
if (gestures.ZoomModifierKey == Keyboard.Modifiers && IsZoomingAllowed())
{
double zoom = Math.Pow(2.0, e.Delta / 3.0 / Mouse.MouseWheelDeltaForOneLine);
Element.ZoomAtPosition(zoom, Element.MouseLocation);
e.Handled = true;
}
}
private bool IsZoomingAllowed()
{
return !Element.DisableZooming
&& (AllowZoomingWhileSelecting || !Element.IsSelecting)
&& (AllowZoomingWhileCutting || !Element.IsCutting)
&& (AllowZoomingWhilePushingItems || !Element.IsPushingItems)
&& (AllowZoomingWhilePanning || !Element.IsPanning);
}
}
}
}

View File

@@ -0,0 +1,98 @@
using System.Collections.Generic;
using System;
using System.Linq;
using System.Windows;
namespace Nodify
{
internal static class AlignmentExtensions
{
public static void Align(this IEnumerable<ItemContainer> values, Alignment alignment, ItemContainer? relativeTo)
{
var containers = values as IReadOnlyCollection<ItemContainer> ?? values.ToList();
switch (alignment)
{
case Alignment.Top:
AlignTop(containers, relativeTo);
break;
case Alignment.Left:
AlignLeft(containers, relativeTo);
break;
case Alignment.Bottom:
AlignBottom(containers, relativeTo);
break;
case Alignment.Right:
AlignRight(containers, relativeTo);
break;
case Alignment.Middle:
AlignMiddle(containers, relativeTo);
break;
case Alignment.Center:
AlignCenter(containers, relativeTo);
break;
default:
throw new ArgumentOutOfRangeException(nameof(alignment), alignment, null);
}
}
private static void AlignTop(IReadOnlyCollection<ItemContainer> containers, ItemContainer? instigator)
{
double top = instigator?.Location.Y ?? containers.Min(x => x.Location.Y);
foreach (var c in containers)
{
c.Location = new Point(c.Location.X, top);
}
}
private static void AlignLeft(IReadOnlyCollection<ItemContainer> containers, ItemContainer? instigator)
{
double left = instigator?.Location.X ?? containers.Min(x => x.Location.X);
foreach (var c in containers)
{
c.Location = new Point(left, c.Location.Y);
}
}
private static void AlignBottom(IReadOnlyCollection<ItemContainer> containers, ItemContainer? instigator)
{
double bottom = instigator != null ? instigator.Location.Y + instigator.ActualHeight : containers.Max(x => x.Location.Y + x.ActualHeight);
foreach (var c in containers)
{
c.Location = new Point(c.Location.X, bottom - c.ActualHeight);
}
}
private static void AlignRight(IReadOnlyCollection<ItemContainer> containers, ItemContainer? instigator)
{
double right = instigator != null ? instigator.Location.X + instigator.ActualWidth : containers.Max(x => x.Location.X + x.ActualWidth);
foreach (var c in containers)
{
c.Location = new Point(right - c.ActualWidth, c.Location.Y);
}
}
private static void AlignMiddle(IReadOnlyCollection<ItemContainer> containers, ItemContainer? instigator)
{
double mid = instigator != null ? instigator.Location.Y + instigator.ActualHeight / 2 : containers.Average(c => c.Location.Y + c.ActualHeight / 2);
foreach (var c in containers)
{
c.Location = new Point(c.Location.X, mid - c.ActualHeight / 2);
}
}
private static void AlignCenter(IReadOnlyCollection<ItemContainer> containers, ItemContainer? instigator)
{
double center = instigator != null ? instigator.Location.X + instigator.ActualWidth / 2 : containers.Average(c => c.Location.X + c.ActualWidth / 2);
foreach (var c in containers)
{
c.Location = new Point(center - c.ActualWidth / 2, c.Location.Y);
}
}
}
}

View File

@@ -0,0 +1,90 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
namespace Nodify
{
/// <summary>
/// Updates the RenderTransform to preview the container position and commits the position changes at the end of the operation.
/// </summary>
internal sealed class DraggingOptimized : IDraggingStrategy
{
private readonly uint _gridCellSize;
private readonly List<ItemContainer> _selectedContainers;
private Vector _dragAccumulator = new Vector(0, 0);
public Vector Offset { get; private set; }
public IReadOnlyCollection<ItemContainer> Containers => _selectedContainers;
public DraggingOptimized(IEnumerable<ItemContainer> containers, uint gridCellSize)
{
_gridCellSize = gridCellSize;
_selectedContainers = containers.Where(c => c.IsDraggable).ToList();
}
public void Abort()
{
for (var i = 0; i < _selectedContainers.Count; i++)
{
ItemContainer container = _selectedContainers[i];
var r = (TranslateTransform)container.RenderTransform;
r.X = 0;
r.Y = 0;
container.OnPreviewLocationChanged(container.Location);
}
_selectedContainers.Clear();
}
public void End()
{
for (var i = 0; i < _selectedContainers.Count; i++)
{
ItemContainer container = _selectedContainers[i];
var r = (TranslateTransform)container.RenderTransform;
Point result = container.Location + new Vector(r.X, r.Y);
// Correct the final position
if (NodifyEditor.EnableSnappingCorrection && (r.X != 0 || r.Y != 0))
{
result.X = (int)result.X / _gridCellSize * _gridCellSize;
result.Y = (int)result.Y / _gridCellSize * _gridCellSize;
}
container.Location = result;
r.X = 0;
r.Y = 0;
}
_selectedContainers.Clear();
}
public void Update(Vector change)
{
_dragAccumulator += change;
var delta = new Vector((int)_dragAccumulator.X / _gridCellSize * _gridCellSize, (int)_dragAccumulator.Y / _gridCellSize * _gridCellSize);
_dragAccumulator -= delta;
if (delta.X != 0 || delta.Y != 0)
{
Offset += delta;
for (var i = 0; i < _selectedContainers.Count; i++)
{
ItemContainer container = _selectedContainers[i];
var r = (TranslateTransform)container.RenderTransform;
r.X += delta.X; // Snapping without correction
r.Y += delta.Y; // Snapping without correction
container.OnPreviewLocationChanged(container.Location + new Vector(r.X, r.Y));
}
}
}
}
}

View File

@@ -0,0 +1,80 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
namespace Nodify
{
internal interface IDraggingStrategy
{
IReadOnlyCollection<ItemContainer> Containers { get; }
Vector Offset { get; }
void Update(Vector change);
void End();
void Abort();
}
internal sealed class DraggingSimple : IDraggingStrategy
{
private readonly uint _gridCellSize;
private readonly List<ItemContainer> _selectedContainers;
private Vector _dragAccumulator = new Vector(0, 0);
public Vector Offset { get; private set; }
public IReadOnlyCollection<ItemContainer> Containers => _selectedContainers;
public DraggingSimple(IEnumerable<ItemContainer> containers, uint gridCellSize)
{
_gridCellSize = gridCellSize;
_selectedContainers = containers.Where(c => c.IsDraggable).ToList();
}
public void Abort()
{
for (var i = 0; i < _selectedContainers.Count; i++)
{
ItemContainer container = _selectedContainers[i];
container.Location -= Offset;
}
_selectedContainers.Clear();
}
public void End()
{
for (var i = 0; i < _selectedContainers.Count; i++)
{
ItemContainer container = _selectedContainers[i];
Point result = container.Location;
// Correct the final position
if (NodifyEditor.EnableSnappingCorrection)
{
result.X = (int)result.X / _gridCellSize * _gridCellSize;
result.Y = (int)result.Y / _gridCellSize * _gridCellSize;
}
container.Location = result;
}
_selectedContainers.Clear();
}
public void Update(Vector change)
{
_dragAccumulator += change;
var delta = new Vector((int)_dragAccumulator.X / _gridCellSize * _gridCellSize, (int)_dragAccumulator.Y / _gridCellSize * _gridCellSize);
_dragAccumulator -= delta;
if (delta.X != 0 || delta.Y != 0)
{
Offset += delta;
for (var i = 0; i < _selectedContainers.Count; i++)
{
ItemContainer container = _selectedContainers[i];
container.Location = new Point(container.Location.X + delta.X, container.Location.Y + delta.Y);
}
}
}
}
}

View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
namespace Nodify
{
internal interface IPushStrategy
{
Rect Start(Point position);
Rect Push(Vector amount);
Rect End();
Rect Cancel();
Rect GetPushedArea();
}
internal abstract class BasePushStrategy : IPushStrategy
{
private const double _minOffset = 2;
private double _actualOffset;
private double _initialPosition;
protected readonly NodifyEditor Editor;
protected const double OffscreenOffset = 100d;
public BasePushStrategy(NodifyEditor editor)
{
Editor = editor;
}
public Rect Start(Point position)
{
var containers = GetFilteredContainers(position);
Editor.BeginDragging(containers);
_initialPosition = GetInitialPosition(position);
_actualOffset = 0;
return CalculatePushedArea(_initialPosition, _actualOffset);
}
public Rect Push(Vector amount)
{
var offset = GetPushOffset(amount);
Editor.UpdateDragging(offset);
_actualOffset += offset.X;
_actualOffset += offset.Y;
double newPosition = _actualOffset >= 0 ? _initialPosition : Editor.SnapToGrid(_initialPosition + _actualOffset);
double newOffset = Math.Max(_minOffset, Editor.SnapToGrid(_actualOffset));
return CalculatePushedArea(newPosition, newOffset);
}
public Rect End()
{
Editor.EndDragging();
return new Rect();
}
public Rect Cancel()
{
Editor.CancelDragging();
return new Rect();
}
protected abstract IEnumerable<ItemContainer> GetFilteredContainers(Point position);
protected abstract double GetInitialPosition(Point position);
protected abstract Vector GetPushOffset(Vector offset);
protected abstract Rect CalculatePushedArea(double position, double offset);
public abstract Rect GetPushedArea();
}
internal sealed class HorizontalPushStrategy : BasePushStrategy
{
public HorizontalPushStrategy(NodifyEditor editor) : base(editor)
{
}
protected override IEnumerable<ItemContainer> GetFilteredContainers(Point position)
=> Editor.ItemContainers.Where(item => item.Location.X >= position.X);
protected override double GetInitialPosition(Point position)
=> position.X;
protected override Vector GetPushOffset(Vector offset)
=> new Vector(offset.X, 0d);
protected override Rect CalculatePushedArea(double position, double offset)
=> new Rect(position, Editor.ViewportLocation.Y - OffscreenOffset, offset, Editor.ViewportSize.Height + OffscreenOffset * 2);
public override Rect GetPushedArea()
=> CalculatePushedArea(Editor.PushedArea.X, Editor.PushedArea.Width);
}
internal sealed class VerticalPushStrategy : BasePushStrategy
{
public VerticalPushStrategy(NodifyEditor editor) : base(editor)
{
}
protected override IEnumerable<ItemContainer> GetFilteredContainers(Point position)
=> Editor.ItemContainers.Where(item => item.Location.Y >= position.Y);
protected override double GetInitialPosition(Point position)
=> position.Y;
protected override Vector GetPushOffset(Vector offset)
=> new Vector(0d, offset.Y);
protected override Rect CalculatePushedArea(double position, double offset)
=> new Rect(Editor.ViewportLocation.X - OffscreenOffset, position, Editor.ViewportSize.Width + OffscreenOffset * 2, offset);
public override Rect GetPushedArea()
=> CalculatePushedArea(Editor.PushedArea.Y, Editor.PushedArea.Height);
}
}