Add project files.
This commit is contained in:
588
Nodify/Connectors/Connector.cs
Normal file
588
Nodify/Connectors/Connector.cs
Normal file
@@ -0,0 +1,588 @@
|
||||
using Nodify.Events;
|
||||
using Nodify.Interactivity;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a connector control that can start and complete a <see cref="PendingConnection"/>.
|
||||
/// Has a <see cref="ElementConnector"/> that the <see cref="Anchor"/> is calculated from for the <see cref="PendingConnection"/>. Center of this control is used if missing.
|
||||
/// </summary>
|
||||
[TemplatePart(Name = ElementConnector, Type = typeof(FrameworkElement))]
|
||||
public class Connector : Control
|
||||
{
|
||||
protected const string ElementConnector = "PART_Connector";
|
||||
|
||||
#region Routed Events
|
||||
|
||||
public static readonly RoutedEvent PendingConnectionStartedEvent = EventManager.RegisterRoutedEvent(nameof(PendingConnectionStarted), RoutingStrategy.Bubble, typeof(PendingConnectionEventHandler), typeof(Connector));
|
||||
public static readonly RoutedEvent PendingConnectionCompletedEvent = EventManager.RegisterRoutedEvent(nameof(PendingConnectionCompleted), RoutingStrategy.Bubble, typeof(PendingConnectionEventHandler), typeof(Connector));
|
||||
public static readonly RoutedEvent PendingConnectionDragEvent = EventManager.RegisterRoutedEvent(nameof(PendingConnectionDrag), RoutingStrategy.Bubble, typeof(PendingConnectionEventHandler), typeof(Connector));
|
||||
public static readonly RoutedEvent DisconnectEvent = EventManager.RegisterRoutedEvent(nameof(Disconnect), RoutingStrategy.Bubble, typeof(ConnectorEventHandler), typeof(Connector));
|
||||
|
||||
/// <summary>Triggered by the <see cref="EditorGestures.ConnectorGestures.Connect"/> gesture.</summary>
|
||||
public event PendingConnectionEventHandler PendingConnectionStarted
|
||||
{
|
||||
add => AddHandler(PendingConnectionStartedEvent, value);
|
||||
remove => RemoveHandler(PendingConnectionStartedEvent, value);
|
||||
}
|
||||
|
||||
/// <summary>Triggered by the <see cref="EditorGestures.ConnectorGestures.Connect"/> gesture.</summary>
|
||||
public event PendingConnectionEventHandler PendingConnectionCompleted
|
||||
{
|
||||
add => AddHandler(PendingConnectionCompletedEvent, value);
|
||||
remove => RemoveHandler(PendingConnectionCompletedEvent, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the mouse is changing position and the <see cref="Connector"/> has mouse capture.
|
||||
/// </summary>
|
||||
public event PendingConnectionEventHandler PendingConnectionDrag
|
||||
{
|
||||
add => AddHandler(PendingConnectionDragEvent, value);
|
||||
remove => RemoveHandler(PendingConnectionDragEvent, value);
|
||||
}
|
||||
|
||||
/// <summary>Triggered by the <see cref="EditorGestures.ConnectorGestures.Disconnect"/> gesture.</summary>
|
||||
public event ConnectorEventHandler Disconnect
|
||||
{
|
||||
add => AddHandler(DisconnectEvent, value);
|
||||
remove => RemoveHandler(DisconnectEvent, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dependency Properties
|
||||
|
||||
public static readonly DependencyProperty AnchorProperty = DependencyProperty.Register(nameof(Anchor), typeof(Point), typeof(Connector), new FrameworkPropertyMetadata(BoxValue.Point));
|
||||
public static readonly DependencyProperty IsConnectedProperty = DependencyProperty.Register(nameof(IsConnected), typeof(bool), typeof(Connector), new FrameworkPropertyMetadata(BoxValue.False, OnIsConnectedChanged));
|
||||
public static readonly DependencyProperty DisconnectCommandProperty = DependencyProperty.Register(nameof(DisconnectCommand), typeof(ICommand), typeof(Connector));
|
||||
private static readonly DependencyPropertyKey IsPendingConnectionPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsPendingConnection), typeof(bool), typeof(Connector), new FrameworkPropertyMetadata(BoxValue.False));
|
||||
public static readonly DependencyProperty IsPendingConnectionProperty = IsPendingConnectionPropertyKey.DependencyProperty;
|
||||
public static readonly DependencyProperty HasCustomContextMenuProperty = NodifyEditor.HasCustomContextMenuProperty.AddOwner(typeof(Connector));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the location in graph space coordinates where <see cref="Connection"/>s can be attached to.
|
||||
/// Bind with <see cref="System.Windows.Data.BindingMode.OneWayToSource"/>
|
||||
/// </summary>
|
||||
public Point Anchor
|
||||
{
|
||||
get => (Point)GetValue(AnchorProperty);
|
||||
set => SetValue(AnchorProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If this is set to false, the <see cref="Disconnect"/> event will not be invoked and the connector will stop updating its <see cref="Anchor"/> when moved, resized etc.
|
||||
/// </summary>
|
||||
public bool IsConnected
|
||||
{
|
||||
get => (bool)GetValue(IsConnectedProperty);
|
||||
set => SetValue(IsConnectedProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value that indicates whether a <see cref="PendingConnection"/> is in progress for this <see cref="Connector"/>.
|
||||
/// </summary>
|
||||
public bool IsPendingConnection
|
||||
{
|
||||
get => (bool)GetValue(IsPendingConnectionProperty);
|
||||
protected set => SetValue(IsPendingConnectionPropertyKey, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked if the <see cref="Disconnect"/> event is not handled.
|
||||
/// Parameter is the <see cref="FrameworkElement.DataContext"/> of this control.
|
||||
/// </summary>
|
||||
public ICommand? DisconnectCommand
|
||||
{
|
||||
get => (ICommand?)GetValue(DisconnectCommandProperty);
|
||||
set => SetValue(DisconnectCommandProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the connector uses a custom context menu.
|
||||
/// </summary>
|
||||
/// <remarks>When set to true, the connector handles the right-click event for specific interactions.</remarks>
|
||||
public bool HasCustomContextMenu
|
||||
{
|
||||
get => (bool)GetValue(HasCustomContextMenuProperty);
|
||||
set => SetValue(HasCustomContextMenuProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the connector has a context menu.
|
||||
/// </summary>
|
||||
public bool HasContextMenu => ContextMenu != null || HasCustomContextMenu;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
private FrameworkElement? _thumb;
|
||||
/// <summary>
|
||||
/// Gets the <see cref="FrameworkElement"/> used to calculate the <see cref="Anchor"/>.
|
||||
/// </summary>
|
||||
protected internal FrameworkElement Thumb => _thumb ??= Template.FindName(ElementConnector, this) as FrameworkElement ?? this;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ItemContainer"/> that contains this <see cref="Connector"/>.
|
||||
/// </summary>
|
||||
public ItemContainer? Container { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="NodifyEditor"/> that owns this <see cref="Container"/>.
|
||||
/// </summary>
|
||||
public NodifyEditor? Editor { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the safe zone outside the editor's viewport that will not trigger optimizations.
|
||||
/// </summary>
|
||||
public static double OptimizeSafeZone = 1000d;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum selected items needed to trigger optimizations when outside of the <see cref="OptimizeSafeZone"/>.
|
||||
/// </summary>
|
||||
public static uint OptimizeMinimumSelectedItems = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if <see cref="Connector"/>s should enable optimizations based on <see cref="OptimizeSafeZone"/> and <see cref="OptimizeMinimumSelectedItems"/>.
|
||||
/// </summary>
|
||||
public static bool EnableOptimizations = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether cancelling a pending connection is allowed.
|
||||
/// </summary>
|
||||
public static bool AllowPendingConnectionCancellation { get; set; } = true;
|
||||
|
||||
private Point _lastUpdatedContainerPosition;
|
||||
private Point _pendingConnectionEndPosition;
|
||||
private bool _isHooked;
|
||||
|
||||
#endregion
|
||||
|
||||
static Connector()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(Connector), new FrameworkPropertyMetadata(typeof(Connector)));
|
||||
FocusableProperty.OverrideMetadata(typeof(Connector), new FrameworkPropertyMetadata(BoxValue.True));
|
||||
}
|
||||
|
||||
public Connector()
|
||||
{
|
||||
InputProcessor.AddSharedHandlers(this);
|
||||
|
||||
Loaded += OnConnectorLoaded;
|
||||
Unloaded += OnConnectorUnloaded;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
Container = this.GetParentOfType<ItemContainer>();
|
||||
Editor = Container?.Editor ?? this.GetParentOfType<NodifyEditor>();
|
||||
}
|
||||
|
||||
#region Update Anchor
|
||||
|
||||
// Toggle events that could be used to update the Anchor
|
||||
private void TrySetAnchorUpdateEvents(bool value)
|
||||
{
|
||||
if (Container != null && Editor != null)
|
||||
{
|
||||
// If events are not already hooked and we are asked to subscribe
|
||||
if (value && !_isHooked)
|
||||
{
|
||||
Container.PreviewLocationChanged += UpdateAnchorOptimized;
|
||||
Container.LocationChanged += OnLocationChanged;
|
||||
Container.SizeChanged += OnContainerSizeChanged;
|
||||
Editor.ViewportUpdated += OnViewportUpdated;
|
||||
_isHooked = true;
|
||||
}
|
||||
// If events are already hooked and we are asked to unsubscribe
|
||||
else if (_isHooked && !value)
|
||||
{
|
||||
Container.PreviewLocationChanged -= UpdateAnchorOptimized;
|
||||
Container.LocationChanged -= OnLocationChanged;
|
||||
Container.SizeChanged -= OnContainerSizeChanged;
|
||||
Editor.ViewportUpdated -= OnViewportUpdated;
|
||||
_isHooked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnContainerSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
=> UpdateAnchorOptimized(Container!.Location);
|
||||
|
||||
private void OnConnectorLoaded(object sender, RoutedEventArgs? e)
|
||||
=> TrySetAnchorUpdateEvents(true);
|
||||
|
||||
private void OnConnectorUnloaded(object sender, RoutedEventArgs e)
|
||||
=> TrySetAnchorUpdateEvents(false);
|
||||
|
||||
private static void OnIsConnectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var con = (Connector)d;
|
||||
|
||||
if ((bool)e.NewValue)
|
||||
{
|
||||
con.UpdateAnchor();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
|
||||
{
|
||||
// Subscribe to events if not already subscribed
|
||||
// Useful for advanced connectors that start collapsed because the loaded event is not called
|
||||
Size newSize = sizeInfo.NewSize;
|
||||
if (newSize.Width > 0d || newSize.Height > 0d)
|
||||
{
|
||||
TrySetAnchorUpdateEvents(true);
|
||||
|
||||
if (Container != null)
|
||||
{
|
||||
UpdateAnchorOptimized(Container!.Location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLocationChanged(object sender, RoutedEventArgs e)
|
||||
=> UpdateAnchorOptimized(Container!.Location);
|
||||
|
||||
private void OnViewportUpdated(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (Container != null && !Container.IsPreviewingLocation && _lastUpdatedContainerPosition != Container.Location)
|
||||
{
|
||||
UpdateAnchorOptimized(Container.Location);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the <see cref="Anchor"/> and applies optimizations if needed based on <see cref="EnableOptimizations"/> flag
|
||||
/// </summary>
|
||||
/// <param name="location"></param>
|
||||
protected void UpdateAnchorOptimized(Point location)
|
||||
{
|
||||
// Update only connectors that are connected
|
||||
if (Editor != null && IsConnected)
|
||||
{
|
||||
bool shouldOptimize = EnableOptimizations && Editor.SelectedContainersCount >= OptimizeMinimumSelectedItems;
|
||||
if (shouldOptimize)
|
||||
{
|
||||
UpdateAnchorBasedOnLocation(Editor, location);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateAnchor(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAnchorBasedOnLocation(NodifyEditor editor, Point location)
|
||||
{
|
||||
var viewport = new Rect(editor.ViewportLocation, editor.ViewportSize);
|
||||
double offset = OptimizeSafeZone / editor.ViewportZoom;
|
||||
|
||||
Rect area = Rect.Inflate(viewport, offset, offset);
|
||||
|
||||
// Update only the connectors that are in the viewport or will be in the viewport
|
||||
if (area.Contains(location))
|
||||
{
|
||||
UpdateAnchor(location);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the <see cref="Anchor"/> relative to a location. (usually <see cref="Container"/>'s location)
|
||||
/// </summary>
|
||||
/// <param name="location">The relative location</param>
|
||||
protected void UpdateAnchor(Point location)
|
||||
{
|
||||
_lastUpdatedContainerPosition = location;
|
||||
|
||||
if (Thumb != null && Container != null)
|
||||
{
|
||||
var thumbSize = (Vector)Thumb.RenderSize;
|
||||
Vector containerMargin = (Vector)Container.RenderSize - (Vector)Container.DesiredSize;
|
||||
Point relativeLocation = Thumb.TranslatePoint((Point)(thumbSize / 2 - containerMargin / 2), Container);
|
||||
Anchor = new Point(location.X + relativeLocation.X, location.Y + relativeLocation.Y);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the <see cref="Anchor"/> based on <see cref="Container"/>'s location.
|
||||
/// </summary>
|
||||
public void UpdateAnchor()
|
||||
{
|
||||
if (Container != null)
|
||||
{
|
||||
UpdateAnchor(Container.Location);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Gesture Handling
|
||||
|
||||
protected InputProcessor InputProcessor { get; } = new InputProcessor();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnMouseDown(MouseButtonEventArgs e)
|
||||
=> InputProcessor.ProcessEvent(e);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnMouseUp(MouseButtonEventArgs e)
|
||||
{
|
||||
InputProcessor.ProcessEvent(e);
|
||||
|
||||
// Release the mouse capture if all the mouse buttons are released and there's no interaction in progress
|
||||
if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released)
|
||||
{
|
||||
ReleaseMouseCapture();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnMouseMove(MouseEventArgs e)
|
||||
=> InputProcessor.ProcessEvent(e);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnMouseWheel(MouseWheelEventArgs e)
|
||||
=> InputProcessor.ProcessEvent(e);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnLostMouseCapture(MouseEventArgs e)
|
||||
=> InputProcessor.ProcessEvent(e);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnKeyUp(KeyEventArgs e)
|
||||
{
|
||||
InputProcessor.ProcessEvent(e);
|
||||
|
||||
// Release the mouse capture if all the mouse buttons are released and there's no interaction in progress
|
||||
if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released)
|
||||
{
|
||||
ReleaseMouseCapture();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
=> InputProcessor.ProcessEvent(e);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a new pending connection from this connector (see <see cref="IsPendingConnection"/>).
|
||||
/// </summary>
|
||||
/// <remarks>This method has no effect if a pending connection is already in progress.</remarks>
|
||||
public void BeginConnecting()
|
||||
=> BeginConnecting(new Vector(0, 0));
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a new pending connection from this connector with the specified offset (see <see cref="IsPendingConnection"/>).
|
||||
/// </summary>
|
||||
/// <remarks>This method has no effect if a pending connection is already in progress.</remarks>
|
||||
public void BeginConnecting(Vector offset)
|
||||
{
|
||||
if (IsPendingConnection)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateAnchor();
|
||||
_pendingConnectionEndPosition = Anchor + offset;
|
||||
|
||||
var args = new PendingConnectionEventArgs(DataContext)
|
||||
{
|
||||
RoutedEvent = PendingConnectionStartedEvent,
|
||||
Anchor = Anchor,
|
||||
Source = this
|
||||
};
|
||||
|
||||
RaiseEvent(args);
|
||||
IsPendingConnection = !args.Canceled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the endpoint of the pending connection by adjusting its position with the specified offset.
|
||||
/// </summary>
|
||||
/// <param name="offset">The amount to adjust the pending connection's endpoint.</param>
|
||||
public void UpdatePendingConnection(Vector offset)
|
||||
=> UpdatePendingConnection(_pendingConnectionEndPosition + offset);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the endpoint of the pending connection to the specified position.
|
||||
/// </summary>
|
||||
/// <param name="position">The new position for the connection's endpoint.</param>
|
||||
public void UpdatePendingConnection(Point position)
|
||||
{
|
||||
//Debug.Assert(IsPendingConnection);
|
||||
|
||||
_pendingConnectionEndPosition = position;
|
||||
|
||||
var args = new PendingConnectionEventArgs(DataContext)
|
||||
{
|
||||
RoutedEvent = PendingConnectionDragEvent,
|
||||
OffsetX = _pendingConnectionEndPosition.X - Anchor.X,
|
||||
OffsetY = _pendingConnectionEndPosition.Y - Anchor.Y,
|
||||
Anchor = Anchor,
|
||||
Source = this
|
||||
};
|
||||
|
||||
RaiseEvent(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the current pending connection without completing it if <see cref="AllowPendingConnectionCancellation"/> is true.
|
||||
/// Otherwise, it completes the pending connection by calling <see cref="EndConnecting()"/>.
|
||||
/// </summary>
|
||||
/// <remarks>This method has no effect if there's no pending connection.</remarks>
|
||||
public void CancelConnecting()
|
||||
{
|
||||
if (!AllowPendingConnectionCancellation)
|
||||
{
|
||||
EndConnecting();
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsPendingConnection)
|
||||
{
|
||||
var args = new PendingConnectionEventArgs(DataContext)
|
||||
{
|
||||
RoutedEvent = PendingConnectionCompletedEvent,
|
||||
Anchor = Anchor,
|
||||
Source = this,
|
||||
Canceled = true
|
||||
};
|
||||
RaiseEvent(args);
|
||||
|
||||
IsPendingConnection = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes the current pending connection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Attempts to identify a target connector near the connection's endpoint and completes the pending connection.
|
||||
/// If no target connector is found, the connection may be completed without a valid target.
|
||||
/// This method has no effect if there's no pending connection.
|
||||
/// </remarks>
|
||||
public void EndConnecting()
|
||||
{
|
||||
if (!IsPendingConnection)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FrameworkElement? elem = FindConnectionTarget(_pendingConnectionEndPosition);
|
||||
EndConnecting(elem?.DataContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes the current pending connection using the specified connector as the target.
|
||||
/// </summary>
|
||||
/// <param name="connector">The connector to use as the connection target.</param>
|
||||
/// <remarks>This method has no effect if there's no pending connection.</remarks>
|
||||
public void EndConnecting(Connector connector)
|
||||
=> EndConnecting(connector.DataContext);
|
||||
|
||||
private void EndConnecting(object? targetDataContext)
|
||||
{
|
||||
if (!IsPendingConnection)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var args = new PendingConnectionEventArgs(DataContext)
|
||||
{
|
||||
TargetConnector = targetDataContext,
|
||||
RoutedEvent = PendingConnectionCompletedEvent,
|
||||
Anchor = Anchor,
|
||||
Source = this
|
||||
};
|
||||
RaiseEvent(args);
|
||||
|
||||
IsPendingConnection = false;
|
||||
_pendingConnectionEndPosition = Anchor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all connections associated with this connector.
|
||||
/// </summary>
|
||||
/// <remarks>This method has no effect if a pending connection is already in progress or the connector is not connected (see <see cref="IsConnected"/>).</remarks>
|
||||
public void RemoveConnections()
|
||||
{
|
||||
if (!IsConnected || IsPendingConnection)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
object? connector = DataContext;
|
||||
var args = new ConnectorEventArgs(connector)
|
||||
{
|
||||
RoutedEvent = DisconnectEvent,
|
||||
Anchor = Anchor,
|
||||
Source = this
|
||||
};
|
||||
|
||||
RaiseEvent(args);
|
||||
|
||||
// Raise DisconnectCommand if event is not handled
|
||||
if (!args.Handled && (DisconnectCommand?.CanExecute(connector) ?? false))
|
||||
{
|
||||
DisconnectCommand.Execute(connector);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translates the event location to graph space coordinates (relative to the <see cref="NodifyEditor.ItemsHost" />).
|
||||
/// </summary>
|
||||
/// <param name="e">The mouse event.</param>
|
||||
/// <remarks>
|
||||
/// Call <see cref="UpdateAnchor()"/> before calling this method if the <see cref="Anchor"/> is not up-to-date.
|
||||
/// </remarks>
|
||||
internal Point GetLocationInsideEditor(MouseEventArgs e)
|
||||
{
|
||||
Vector thumbOffset = e.GetPosition(Thumb) - new Point(Thumb.ActualWidth / 2, Thumb.ActualHeight / 2);
|
||||
return Anchor + thumbOffset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for a <see cref="Connector"/> at the specified position.
|
||||
/// </summary>
|
||||
/// <param name="position">The position in the editor to check for a connector.</param>
|
||||
public Connector? FindTargetConnector(Point position)
|
||||
{
|
||||
if (Editor != null)
|
||||
{
|
||||
return (Connector?)PendingConnection.GetPotentialConnector(Editor, position, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for a potential <see cref="Connector"/> or <see cref="ItemContainer"/> at the specified position within the editor.
|
||||
/// </summary>
|
||||
/// <param name="position">The position in the editor to check for a potential connection target.</param>
|
||||
public FrameworkElement? FindConnectionTarget(Point position)
|
||||
{
|
||||
if (Editor != null)
|
||||
{
|
||||
return PendingConnection.GetPotentialConnector(Editor, position);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
38
Nodify/Connectors/Events/ConnectorEventArgs.cs
Normal file
38
Nodify/Connectors/Events/ConnectorEventArgs.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the method that will handle <see cref="Connector"/> related routed events.
|
||||
/// </summary>
|
||||
/// <param name="sender">The object where the event handler is attached.</param>
|
||||
/// <param name="e">The event data.</param>
|
||||
public delegate void ConnectorEventHandler(object sender, ConnectorEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for <see cref="Nodify.Connector"/> related routed events.
|
||||
/// </summary>
|
||||
public class ConnectorEventArgs : RoutedEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectorEventArgs"/> class using the specified <see cref="Connector"/>.
|
||||
/// </summary>
|
||||
/// <param name="connector">The <see cref="FrameworkElement.DataContext"/> of a related <see cref="Nodify.Connector"/>.</param>
|
||||
public ConnectorEventArgs(object connector)
|
||||
=> Connector = connector;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Nodify.Connector.Anchor"/> of the <see cref="Nodify.Connector"/> associated with this event.
|
||||
/// </summary>
|
||||
public Point Anchor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="FrameworkElement.DataContext"/> of the <see cref="Nodify.Connector"/> associated with this event.
|
||||
/// </summary>
|
||||
public object Connector { get; }
|
||||
|
||||
protected override void InvokeEventHandler(Delegate genericHandler, object genericTarget)
|
||||
=> ((ConnectorEventHandler)genericHandler)(genericTarget, this);
|
||||
}
|
||||
}
|
||||
58
Nodify/Connectors/Events/PendingConnectionEventArgs.cs
Normal file
58
Nodify/Connectors/Events/PendingConnectionEventArgs.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the method that will handle <see cref="PendingConnection"/> related routed events.
|
||||
/// </summary>
|
||||
/// <param name="sender">The object where the event handler is attached.</param>
|
||||
/// <param name="e">The event data.</param>
|
||||
public delegate void PendingConnectionEventHandler(object sender, PendingConnectionEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for <see cref="PendingConnection"/> related routed events.
|
||||
/// </summary>
|
||||
public class PendingConnectionEventArgs : RoutedEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PendingConnectionEventArgs"/> class using the specified <see cref="SourceConnector"/>.
|
||||
/// </summary>
|
||||
/// <param name="sourceConnector">The <see cref="FrameworkElement.DataContext"/> of a related <see cref="Connector"/>.</param>
|
||||
public PendingConnectionEventArgs(object sourceConnector)
|
||||
=> SourceConnector = sourceConnector;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Connector.Anchor"/> of the <see cref="Connector"/> that raised this event.
|
||||
/// </summary>
|
||||
public Point Anchor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="FrameworkElement.DataContext"/> of the <see cref="Connector"/> that started this <see cref="PendingConnection"/>.
|
||||
/// </summary>
|
||||
public object SourceConnector { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="FrameworkElement.DataContext"/> of the target <see cref="Connector"/> when the <see cref="PendingConnection"/> is completed.
|
||||
/// </summary>
|
||||
public object? TargetConnector { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the distance from the <see cref="SourceConnector"/> in the X axis.
|
||||
/// </summary>
|
||||
public double OffsetX { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the distance from the <see cref="SourceConnector"/> in the Y axis.
|
||||
/// </summary>
|
||||
public double OffsetY { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether this <see cref="PendingConnection"/> was cancelled.
|
||||
/// </summary>
|
||||
public bool Canceled { get; set; }
|
||||
|
||||
protected override void InvokeEventHandler(Delegate genericHandler, object genericTarget)
|
||||
=> ((PendingConnectionEventHandler)genericHandler)(genericTarget, this);
|
||||
}
|
||||
}
|
||||
21
Nodify/Connectors/HotKeyControl.cs
Normal file
21
Nodify/Connectors/HotKeyControl.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Windows.Controls;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
public class HotKeyControl : Control
|
||||
{
|
||||
public static readonly DependencyProperty NumberProperty = DependencyProperty.Register(nameof(Number), typeof(int), typeof(HotKeyControl), new PropertyMetadata(BoxValue.Int0));
|
||||
|
||||
public int Number
|
||||
{
|
||||
get => (int)GetValue(NumberProperty);
|
||||
set => SetValue(NumberProperty, value);
|
||||
}
|
||||
|
||||
static HotKeyControl()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(HotKeyControl), new FrameworkPropertyMetadata(typeof(HotKeyControl)));
|
||||
}
|
||||
}
|
||||
}
|
||||
621
Nodify/Connectors/PendingConnection.cs
Normal file
621
Nodify/Connectors/PendingConnection.cs
Normal file
@@ -0,0 +1,621 @@
|
||||
using Nodify.Events;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies how hotkeys are displayed for a pending connection.
|
||||
/// </summary>
|
||||
public enum HotKeysDisplayMode
|
||||
{
|
||||
/// <summary>
|
||||
/// No hotkeys will be displayed for the pending connection.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Display hotkeys for keyboard only.
|
||||
/// </summary>
|
||||
Keyboard,
|
||||
|
||||
/// <summary>
|
||||
/// Display hotkeys for both mouse and keyboard.
|
||||
/// </summary>
|
||||
All
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a pending connection usually started by a <see cref="Connector"/> which invokes the <see cref="CompletedCommand"/> when completed.
|
||||
/// </summary>
|
||||
public class PendingConnection : ContentControl
|
||||
{
|
||||
#region Dependency Properties
|
||||
|
||||
public static readonly DependencyProperty SourceAnchorProperty = DependencyProperty.Register(nameof(SourceAnchor), typeof(Point), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.Point, FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
public static readonly DependencyProperty TargetAnchorProperty = DependencyProperty.Register(nameof(TargetAnchor), typeof(Point), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.Point, FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(object), typeof(PendingConnection));
|
||||
public static readonly DependencyProperty TargetProperty = DependencyProperty.Register(nameof(Target), typeof(object), typeof(PendingConnection));
|
||||
public static readonly DependencyProperty PreviewTargetProperty = DependencyProperty.Register(nameof(PreviewTarget), typeof(object), typeof(PendingConnection));
|
||||
public static readonly DependencyProperty EnablePreviewProperty = DependencyProperty.Register(nameof(EnablePreview), typeof(bool), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.False));
|
||||
public static readonly DependencyProperty StrokeThicknessProperty = Shape.StrokeThicknessProperty.AddOwner(typeof(PendingConnection));
|
||||
public static readonly DependencyProperty StrokeDashArrayProperty = Shape.StrokeDashArrayProperty.AddOwner(typeof(PendingConnection));
|
||||
public static readonly DependencyProperty StrokeProperty = Shape.StrokeProperty.AddOwner(typeof(PendingConnection));
|
||||
public static readonly DependencyProperty AllowOnlyConnectorsProperty = DependencyProperty.Register(nameof(AllowOnlyConnectors), typeof(bool), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.True, OnAllowOnlyConnectorsChanged));
|
||||
public static readonly DependencyProperty EnableSnappingProperty = DependencyProperty.Register(nameof(EnableSnapping), typeof(bool), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.False));
|
||||
public static readonly DependencyProperty DirectionProperty = BaseConnection.DirectionProperty.AddOwner(typeof(PendingConnection));
|
||||
public new static readonly DependencyProperty IsVisibleProperty = DependencyProperty.Register(nameof(IsVisible), typeof(bool), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.False, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnVisibilityChanged));
|
||||
|
||||
private static void OnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var connection = (PendingConnection)d;
|
||||
connection.Visibility = ((bool)e.NewValue) ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the starting point for the connection.
|
||||
/// </summary>
|
||||
public Point SourceAnchor
|
||||
{
|
||||
get => (Point)GetValue(SourceAnchorProperty);
|
||||
set => SetValue(SourceAnchorProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the end point for the connection.
|
||||
/// </summary>
|
||||
public Point TargetAnchor
|
||||
{
|
||||
get => (Point)GetValue(TargetAnchorProperty);
|
||||
set => SetValue(TargetAnchorProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Connector"/>'s <see cref="FrameworkElement.DataContext"/> that started this pending connection.
|
||||
/// </summary>
|
||||
public object? Source
|
||||
{
|
||||
get => GetValue(SourceProperty);
|
||||
set => SetValue(SourceProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Connector"/>'s <see cref="FrameworkElement.DataContext"/> (or potentially an <see cref="ItemContainer"/>'s <see cref="FrameworkElement.DataContext"/> if <see cref="AllowOnlyConnectors"/> is false) that the <see cref="Source"/> can connect to.
|
||||
/// Only set when the connection is completed (see <see cref="CompletedCommand"/>).
|
||||
/// </summary>
|
||||
public object? Target
|
||||
{
|
||||
get => GetValue(TargetProperty);
|
||||
set => SetValue(TargetProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="PreviewTarget"/> will be updated with a potential <see cref="Connector"/>'s <see cref="FrameworkElement.DataContext"/> if this is true.
|
||||
/// </summary>
|
||||
/// <remarks>Requires <see cref="EnableHitTesting"/> to be true.</remarks>
|
||||
public bool EnablePreview
|
||||
{
|
||||
get => (bool)GetValue(EnablePreviewProperty);
|
||||
set => SetValue(EnablePreviewProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Connector"/> or the <see cref="ItemContainer"/> (if <see cref="AllowOnlyConnectors"/> is false) that we're previewing. See <see cref="EnablePreview"/>.
|
||||
/// </summary>
|
||||
public object? PreviewTarget
|
||||
{
|
||||
get => GetValue(PreviewTargetProperty);
|
||||
set => SetValue(PreviewTargetProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables snapping the <see cref="TargetAnchor"/> to a possible <see cref="Target"/> connector.
|
||||
/// </summary>
|
||||
/// <remarks>Requires <see cref="EnableHitTesting"/> to be true.</remarks>
|
||||
public bool EnableSnapping
|
||||
{
|
||||
get => (bool)GetValue(EnableSnappingProperty);
|
||||
set => SetValue(EnableSnappingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If true will preview and connect only to <see cref="Connector"/>s, otherwise will also enable <see cref="ItemContainer"/>s.
|
||||
/// </summary>
|
||||
public bool AllowOnlyConnectors
|
||||
{
|
||||
get => (bool)GetValue(AllowOnlyConnectorsProperty);
|
||||
set => SetValue(AllowOnlyConnectorsProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or set the connection thickness.
|
||||
/// </summary>
|
||||
public double StrokeThickness
|
||||
{
|
||||
get => (double)GetValue(StrokeThicknessProperty);
|
||||
set => SetValue(StrokeThicknessProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pattern of dashes and gaps that is used to outline the connection.
|
||||
/// </summary>
|
||||
public DoubleCollection StrokeDashArray
|
||||
{
|
||||
get => (DoubleCollection)GetValue(StrokeDashArrayProperty);
|
||||
set => SetValue(StrokeDashArrayProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the stroke color of the connection.
|
||||
/// </summary>
|
||||
public Brush Stroke
|
||||
{
|
||||
get => (Brush)GetValue(StrokeProperty);
|
||||
set => SetValue(StrokeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the visibility of the connection.
|
||||
/// </summary>
|
||||
public new bool IsVisible
|
||||
{
|
||||
get => base.IsVisible;
|
||||
set => SetValue(IsVisibleProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the direction of this connection.
|
||||
/// </summary>
|
||||
public ConnectionDirection Direction
|
||||
{
|
||||
get => (ConnectionDirection)GetValue(DirectionProperty);
|
||||
set => SetValue(DirectionProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Attached Properties
|
||||
|
||||
private static readonly DependencyProperty AllowOnlyConnectorsAttachedProperty = DependencyProperty.RegisterAttached("AllowOnlyConnectorsAttached", typeof(bool), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.True));
|
||||
/// <summary>
|
||||
/// Will be set for <see cref="Connector"/>s and <see cref="ItemContainer"/>s when the pending connection is over the element if <see cref="EnablePreview"/> or <see cref="EnableSnapping"/> is true.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty IsOverElementProperty = DependencyProperty.RegisterAttached("IsOverElement", typeof(bool), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.False));
|
||||
|
||||
internal static bool GetAllowOnlyConnectorsAttached(UIElement elem)
|
||||
=> (bool)elem.GetValue(AllowOnlyConnectorsAttachedProperty);
|
||||
|
||||
internal static void SetAllowOnlyConnectorsAttached(UIElement elem, bool value)
|
||||
=> elem.SetValue(AllowOnlyConnectorsAttachedProperty, value);
|
||||
|
||||
public static bool GetIsOverElement(UIElement elem)
|
||||
=> (bool)elem.GetValue(IsOverElementProperty);
|
||||
|
||||
public static void SetIsOverElement(UIElement elem, bool value)
|
||||
=> elem.SetValue(IsOverElementProperty, value);
|
||||
|
||||
private static void OnAllowOnlyConnectorsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
NodifyEditor? editor = ((PendingConnection)d).Editor;
|
||||
|
||||
if (editor != null)
|
||||
{
|
||||
SetAllowOnlyConnectorsAttached(editor, (bool)e.NewValue);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Commands
|
||||
|
||||
public static readonly DependencyProperty StartedCommandProperty = DependencyProperty.Register(nameof(StartedCommand), typeof(ICommand), typeof(PendingConnection));
|
||||
public static readonly DependencyProperty CompletedCommandProperty = DependencyProperty.Register(nameof(CompletedCommand), typeof(ICommand), typeof(PendingConnection));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the command to invoke when the pending connection is started.
|
||||
/// Will not be invoked if <see cref="NodifyEditor.ConnectionStartedCommand"/> is used.
|
||||
/// <see cref="Source"/> will be set to the <see cref="Connector"/>'s <see cref="FrameworkElement.DataContext"/> that started this connection and will also be the command's parameter.
|
||||
/// </summary>
|
||||
public ICommand? StartedCommand
|
||||
{
|
||||
get => (ICommand?)GetValue(StartedCommandProperty);
|
||||
set => SetValue(StartedCommandProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the command to invoke when the pending connection is completed.
|
||||
/// Will not be invoked if <see cref="NodifyEditor.ConnectionCompletedCommand"/> is used.
|
||||
/// <see cref="Target"/> will be set to the desired <see cref="Connector"/>'s <see cref="FrameworkElement.DataContext"/> and will also be the command's parameter.
|
||||
/// </summary>
|
||||
public ICommand? CompletedCommand
|
||||
{
|
||||
get => (ICommand?)GetValue(CompletedCommandProperty);
|
||||
set => SetValue(CompletedCommandProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether hit testing is enabled for pending connections.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// - When enabled, the <see cref="IsOverElementProperty"/> is updated on connectors during the drag operation. <br />
|
||||
/// - When disabled, the <see cref="EnablePreview"/> and <see cref="EnableSnapping"/> properties will have no effect. <br />
|
||||
/// - Disable hit testing to improve performance.
|
||||
/// </remarks>
|
||||
public static bool EnableHitTesting { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of hotkeys that can be displayed for a pending connection.
|
||||
/// </summary>
|
||||
/// <remarks>The maximum value can be 9.</remarks>
|
||||
public static uint MaxHotKeys { get; set; } = 9;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether hotkeys are enabled for pending connections.
|
||||
/// </summary>
|
||||
public static HotKeysDisplayMode HotKeysDisplayMode { get; set; } = HotKeysDisplayMode.Keyboard;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="NodifyEditor"/> that owns this <see cref="PendingConnection"/>.
|
||||
/// </summary>
|
||||
protected NodifyEditor? Editor { get; private set; }
|
||||
|
||||
private FrameworkElement? _connectionTarget;
|
||||
private Connector? _hotKeysSource;
|
||||
private readonly List<HotKeyAdorner> _hotKeysAdorners = new List<HotKeyAdorner>();
|
||||
private AdornerLayer? _adornerLayer;
|
||||
|
||||
private AdornerLayer AdornerLayer => _adornerLayer ??= AdornerLayer.GetAdornerLayer(this);
|
||||
|
||||
#endregion
|
||||
|
||||
static PendingConnection()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(PendingConnection), new FrameworkPropertyMetadata(typeof(PendingConnection)));
|
||||
IsHitTestVisibleProperty.OverrideMetadata(typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.False));
|
||||
IsEnabledProperty.OverrideMetadata(typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.False));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
if (Editor != null)
|
||||
{
|
||||
Editor.RemoveHandler(Connector.PendingConnectionStartedEvent, new PendingConnectionEventHandler(OnPendingConnectionStarted));
|
||||
Editor.RemoveHandler(Connector.PendingConnectionDragEvent, new PendingConnectionEventHandler(OnPendingConnectionDrag));
|
||||
Editor.RemoveHandler(Connector.PendingConnectionCompletedEvent, new PendingConnectionEventHandler(OnPendingConnectionCompleted));
|
||||
Editor.RemoveHandler(PreviewKeyUpEvent, new KeyEventHandler(OnKeyUp));
|
||||
}
|
||||
|
||||
Editor = this.GetParentOfType<NodifyEditor>();
|
||||
|
||||
if (Editor != null)
|
||||
{
|
||||
Editor.AddHandler(Connector.PendingConnectionStartedEvent, new PendingConnectionEventHandler(OnPendingConnectionStarted));
|
||||
Editor.AddHandler(Connector.PendingConnectionDragEvent, new PendingConnectionEventHandler(OnPendingConnectionDrag));
|
||||
Editor.AddHandler(Connector.PendingConnectionCompletedEvent, new PendingConnectionEventHandler(OnPendingConnectionCompleted));
|
||||
Editor.AddHandler(PreviewKeyUpEvent, new KeyEventHandler(OnKeyUp), true);
|
||||
|
||||
SetAllowOnlyConnectorsAttached(Editor, AllowOnlyConnectors);
|
||||
}
|
||||
}
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
protected virtual void OnPendingConnectionStarted(object sender, PendingConnectionEventArgs e)
|
||||
{
|
||||
if (!e.Handled && !e.Canceled)
|
||||
{
|
||||
e.Handled = true;
|
||||
e.Canceled = !StartedCommand?.CanExecute(e.SourceConnector) ?? false;
|
||||
|
||||
Target = null;
|
||||
IsVisible = !e.Canceled;
|
||||
SourceAnchor = e.Anchor;
|
||||
TargetAnchor = new Point(e.Anchor.X + e.OffsetX, e.Anchor.Y + e.OffsetY);
|
||||
Source = e.SourceConnector;
|
||||
|
||||
if (!e.Canceled)
|
||||
{
|
||||
StartedCommand?.Execute(Source);
|
||||
|
||||
if (e.OriginalSource is Connector connector)
|
||||
{
|
||||
ShowHotKeys(connector);
|
||||
}
|
||||
}
|
||||
|
||||
if (EnablePreview)
|
||||
{
|
||||
PreviewTarget = e.SourceConnector;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnPendingConnectionDrag(object sender, PendingConnectionEventArgs e)
|
||||
{
|
||||
if (!e.Handled && IsVisible)
|
||||
{
|
||||
e.Handled = true;
|
||||
TargetAnchor = new Point(e.Anchor.X + e.OffsetX, e.Anchor.Y + e.OffsetY);
|
||||
|
||||
if (!EnableHitTesting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Look for a potential connector
|
||||
FrameworkElement? target = FindConnectionTarget(TargetAnchor);
|
||||
|
||||
// Update the connector's anchor and snap to it, if snapping is enabled
|
||||
if (EnableSnapping && target is Connector connector)
|
||||
{
|
||||
connector.UpdateAnchor();
|
||||
TargetAnchor = connector.Anchor;
|
||||
}
|
||||
|
||||
SetConnectionTarget(target);
|
||||
|
||||
// Update the preview target if enabled
|
||||
if (EnablePreview)
|
||||
{
|
||||
PreviewTarget = target?.DataContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnPendingConnectionCompleted(object sender, PendingConnectionEventArgs e)
|
||||
{
|
||||
if (!e.Handled && IsVisible)
|
||||
{
|
||||
e.Handled = true;
|
||||
IsVisible = false;
|
||||
|
||||
SetConnectionTarget(null);
|
||||
HideHotKeys();
|
||||
|
||||
if (!e.Canceled)
|
||||
{
|
||||
Target = e.TargetConnector;
|
||||
|
||||
// Invoke the CompletedCommand if event is not handled
|
||||
if (CompletedCommand?.CanExecute(Target) ?? false)
|
||||
{
|
||||
CompletedCommand?.Execute(Target);
|
||||
}
|
||||
}
|
||||
|
||||
if (EnablePreview)
|
||||
{
|
||||
PreviewTarget = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the connection target and updates the visual state of the target element.
|
||||
/// </summary>
|
||||
private void SetConnectionTarget(FrameworkElement? target)
|
||||
{
|
||||
if (target == _connectionTarget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_connectionTarget != null)
|
||||
{
|
||||
SetIsOverElement(_connectionTarget, false);
|
||||
}
|
||||
|
||||
if (target != null)
|
||||
{
|
||||
SetIsOverElement(target, true);
|
||||
}
|
||||
|
||||
_connectionTarget = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for a potential <see cref="Connector"/> or <see cref="ItemContainer"/> at the specified position within the editor.
|
||||
/// </summary>
|
||||
public FrameworkElement? FindConnectionTarget(Point position)
|
||||
{
|
||||
if (Editor != null)
|
||||
{
|
||||
return GetPotentialConnector(Editor, position, AllowOnlyConnectors);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Hot Keys
|
||||
|
||||
private void OnKeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (_hotKeysSource is { IsPendingConnection: true })
|
||||
{
|
||||
int hotKey = GetHotKey(e.Key);
|
||||
|
||||
if (hotKey <= _hotKeysAdorners.Count)
|
||||
{
|
||||
var adorner = _hotKeysAdorners.Find(x => x.Number == hotKey);
|
||||
if (adorner != null)
|
||||
{
|
||||
var tempConnector = _hotKeysSource;
|
||||
_hotKeysSource.EndConnecting(adorner.Connector);
|
||||
tempConnector.ReleaseMouseCapture();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetHotKey(Key key)
|
||||
{
|
||||
if (key >= Key.D1 && key <= Key.D9)
|
||||
{
|
||||
return key - Key.D0;
|
||||
}
|
||||
|
||||
if (key >= Key.NumPad1 && key <= Key.NumPad9)
|
||||
{
|
||||
return key - Key.NumPad0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void ShowHotKeys(Connector sourceConnector)
|
||||
{
|
||||
if (Editor == null
|
||||
|| AdornerLayer == null
|
||||
|| HotKeysDisplayMode == HotKeysDisplayMode.None
|
||||
|| HotKeysDisplayMode == HotKeysDisplayMode.Keyboard && !(InputManager.Current.MostRecentInputDevice is KeyboardDevice))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_hotKeysSource = sourceConnector;
|
||||
|
||||
var connectCommand = Editor.ConnectionCompletedCommand ?? CompletedCommand;
|
||||
if (connectCommand != null)
|
||||
{
|
||||
bool isEditorConnect = connectCommand == Editor.ConnectionCompletedCommand;
|
||||
var connectorsInViewport = Editor.GetConnectorsInViewport();
|
||||
|
||||
var possibleTargets = connectorsInViewport
|
||||
.Where(x => isEditorConnect ? connectCommand.CanExecute((sourceConnector.DataContext, x.DataContext)) : connectCommand.CanExecute(x.DataContext))
|
||||
.OrderBy(x => (sourceConnector.Anchor - x.Anchor).LengthSquared)
|
||||
.Take((int)Math.Min(MaxHotKeys, 9));
|
||||
|
||||
var adorners = possibleTargets.Select((x, i) => new HotKeyAdorner(x, i + 1));
|
||||
|
||||
foreach (var adorner in adorners)
|
||||
{
|
||||
_hotKeysAdorners.Add(adorner);
|
||||
AdornerLayer.Add(adorner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HideHotKeys()
|
||||
{
|
||||
_hotKeysSource = null;
|
||||
|
||||
if (AdornerLayer != null)
|
||||
{
|
||||
foreach (var hotKeyAdorner in _hotKeysAdorners)
|
||||
{
|
||||
AdornerLayer.Remove(hotKeyAdorner);
|
||||
}
|
||||
|
||||
_hotKeysAdorners.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Searches for a potential <see cref="Connector"/> or <see cref="ItemContainer"/> at the specified position within the editor.
|
||||
/// </summary>
|
||||
/// <param name="editor">The <see cref="NodifyEditor"/> to scan for connectors or item containers.</param>
|
||||
/// <param name="position">The position in the editor to check for intersections.</param>
|
||||
/// <param name="allowOnlyConnectors">
|
||||
/// If true, only <see cref="Connector"/>s are considered; otherwise, the method will also check for <see cref="ItemContainer"/>s.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Returns one of the following, depending on what is found at the specified position:
|
||||
/// <br /> - A <see cref="Connector"/> if one is present.
|
||||
/// <br /> - An <see cref="ItemContainer"/> if <paramref name="allowOnlyConnectors"/> is false and a <see cref="Connector"/> is not found.
|
||||
/// <br /> - The provided <see cref="NodifyEditor"/> itself if neither a <see cref="Connector"/> nor an <see cref="ItemContainer" /> is found, and <paramref name="allowOnlyConnectors"/> is true.
|
||||
/// <br /> - Null if no valid element is identified at the specified position.
|
||||
/// </returns>
|
||||
internal static FrameworkElement? GetPotentialConnector(NodifyEditor editor, Point position, bool allowOnlyConnectors)
|
||||
{
|
||||
Connector? connector = editor.ItemsHost.GetElementAtPosition<Connector>(position);
|
||||
if (connector != null && connector.Editor == editor)
|
||||
return connector;
|
||||
|
||||
if (allowOnlyConnectors)
|
||||
return null;
|
||||
|
||||
var itemContainer = editor.ItemsHost.GetElementAtPosition<ItemContainer>(position);
|
||||
if (itemContainer != null && itemContainer.Editor == editor)
|
||||
return itemContainer;
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for a potential <see cref="Connector"/> or <see cref="ItemContainer"/> at the specified position,
|
||||
/// automatically determining whether to prioritize connectors based on editor settings.
|
||||
/// </summary>
|
||||
/// <param name="editor">The <see cref="NodifyEditor"/> to scan.</param>
|
||||
/// <param name="position">The position in the editor to check for intersections.</param>
|
||||
/// <returns>
|
||||
/// Returns a <see cref="Connector"/>, an <see cref="ItemContainer"/>, the <see cref="NodifyEditor"/>, or null.
|
||||
/// </returns>
|
||||
internal static FrameworkElement? GetPotentialConnector(NodifyEditor editor, Point position)
|
||||
=> GetPotentialConnector(editor, position, GetAllowOnlyConnectorsAttached(editor));
|
||||
|
||||
#endregion
|
||||
|
||||
private class HotKeyAdorner : Adorner
|
||||
{
|
||||
private readonly HotKeyControl _hotKeyControl;
|
||||
public Connector Connector { get; }
|
||||
public int Number { get; }
|
||||
private Point _offset;
|
||||
|
||||
public HotKeyAdorner(Connector connector, int number) : base(connector)
|
||||
{
|
||||
IsHitTestVisible = false;
|
||||
Connector = connector;
|
||||
Number = number;
|
||||
|
||||
_hotKeyControl = new HotKeyControl
|
||||
{
|
||||
Number = number,
|
||||
DataContext = connector.DataContext
|
||||
};
|
||||
|
||||
AddVisualChild(_hotKeyControl);
|
||||
AddLogicalChild(_hotKeyControl);
|
||||
|
||||
_offset = connector.Thumb.TranslatePoint(new Point(0, 0), connector);
|
||||
}
|
||||
|
||||
protected override int VisualChildrenCount => 1;
|
||||
|
||||
protected override Visual GetVisualChild(int index) => _hotKeyControl;
|
||||
|
||||
protected override Size MeasureOverride(Size constraint)
|
||||
{
|
||||
_hotKeyControl.Measure(constraint);
|
||||
return _hotKeyControl.DesiredSize;
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
_hotKeyControl.Arrange(new Rect(_offset, finalSize));
|
||||
return finalSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
Nodify/Connectors/States/Connecting.cs
Normal file
44
Nodify/Connectors/States/Connecting.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
public static partial class ConnectorState
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the state for handling a connector's connecting operation in the editor,
|
||||
/// enabling drag-based creation of connections between connectors.
|
||||
/// </summary>
|
||||
public class Connecting : DragState<Connector>
|
||||
{
|
||||
protected override bool HasContextMenu => Element.HasContextMenu;
|
||||
protected override bool CanCancel => Connector.AllowPendingConnectionCancellation;
|
||||
protected override bool IsToggle => EnableToggledConnectingMode;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Connecting"/> class.
|
||||
/// </summary>
|
||||
/// <param name="connector">The connector associated with this state.</param>
|
||||
public Connecting(Connector connector)
|
||||
: base(connector, EditorGestures.Mappings.Connector.Connect, EditorGestures.Mappings.Connector.CancelAction)
|
||||
{
|
||||
PositionElement = Element.Editor ?? (IInputElement)Element;
|
||||
}
|
||||
|
||||
protected override void OnBegin(InputEventArgs e)
|
||||
=> Element.BeginConnecting();
|
||||
|
||||
protected override void OnEnd(InputEventArgs e)
|
||||
=> Element.EndConnecting();
|
||||
|
||||
protected override void OnCancel(InputEventArgs e)
|
||||
=> Element.CancelConnecting();
|
||||
|
||||
protected override void OnMouseMove(MouseEventArgs e)
|
||||
{
|
||||
Point editorPosition = Element.GetLocationInsideEditor(e); // could also use Element.Editor.MouseLocation
|
||||
Element.UpdatePendingConnection(editorPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Nodify/Connectors/States/ConnectorState.cs
Normal file
17
Nodify/Connectors/States/ConnectorState.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
public static partial class ConnectorState
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether toggled connecting mode is enabled, allowing the user to start and end the interaction in two steps with the same input gesture.
|
||||
/// </summary>
|
||||
public static bool EnableToggledConnectingMode { get; set; }
|
||||
|
||||
internal static void RegisterDefaultHandlers()
|
||||
{
|
||||
InputProcessor.Shared<Connector>.RegisterHandlerFactory(elem => new Disconnect(elem));
|
||||
InputProcessor.Shared<Connector>.RegisterHandlerFactory(elem => new Connecting(elem));
|
||||
InputProcessor.Shared<Connector>.RegisterHandlerFactory(elem => new Default(elem));
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Nodify/Connectors/States/Default.cs
Normal file
31
Nodify/Connectors/States/Default.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
public static partial class ConnectorState
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the default input state for a <see cref="Connector"/>.
|
||||
/// </summary>
|
||||
public class Default : InputElementState<Connector>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Default"/> class.
|
||||
/// </summary>
|
||||
/// <param name="connector">The <see cref="Connector"/> element associated with this state.</param>
|
||||
public Default(Connector connector) : base(connector)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnMouseDown(MouseButtonEventArgs e)
|
||||
{
|
||||
// Allow context menu to appear
|
||||
if (e.ChangedButton == MouseButton.Right && Element.HasContextMenu)
|
||||
{
|
||||
Element.Focus();
|
||||
e.Handled = true; // prevents the editor capturing the mouse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Nodify/Connectors/States/Disconnect.cs
Normal file
51
Nodify/Connectors/States/Disconnect.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
public static partial class ConnectorState
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a state in which a connector can be disconnected from its connections based on specific gestures.
|
||||
/// </summary>
|
||||
public class Disconnect : InputElementState<Connector>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Disconnect"/> class.
|
||||
/// </summary>
|
||||
/// <param name="connector">The <see cref="Connector"/> element associated with this state.</param>
|
||||
public Disconnect(Connector connector) : base(connector)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnMouseDown(MouseButtonEventArgs e)
|
||||
{
|
||||
EditorGestures.ConnectorGestures gestures = EditorGestures.Mappings.Connector;
|
||||
if (gestures.Disconnect.Matches(e.Source, e))
|
||||
{
|
||||
Element.Focus();
|
||||
e.Handled = true; // prevent interacting with the container
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseButtonEventArgs e)
|
||||
{
|
||||
EditorGestures.ConnectorGestures gestures = EditorGestures.Mappings.Connector;
|
||||
if (gestures.Disconnect.Matches(e.Source, e))
|
||||
{
|
||||
Element.RemoveConnections();
|
||||
e.Handled = true; // prevent opening context menu
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
EditorGestures.ConnectorGestures gestures = EditorGestures.Mappings.Connector;
|
||||
if (gestures.Disconnect.Matches(e.Source, e))
|
||||
{
|
||||
Element.RemoveConnections();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user