Skip to content

Commit

Permalink
Merge pull request #177 from miroiu/feature/146-connection
Browse files Browse the repository at this point in the history
Enable custom input processing for BaseConnection
  • Loading branch information
miroiu authored Dec 13, 2024
2 parents f1ae858 + d5db711 commit 3786ac7
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 22 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@
> - Added PreserveSelectionOnRightClick configuration field to ItemContainer
> - Added BeginConnecting, UpdatePendingConnection, EndConnecting, CancelConnecting and RemoveConnections methods to Connector
> - Added a custom MouseGesture with support for key combinations
> - Added InputProcessor to NodifyEditor, ItemContainer, Connector and Minimap, enabling the extension of controls with custom states
> - Added InputProcessor to NodifyEditor, ItemContainer, Connector, BaseConnection and Minimap, enabling the extension of controls with custom states
> - Added DragState to simplify creating click-and-drag operations, with support for initiating and completing them using the keyboard
> - Added InputElementStateStack to manage transitions between states in UI elements
> - Added InputProcessor.Shared to enable the addition of global input handlers
> - Move the viewport to the mouse position when zooming on the Minimap if ResizeToViewport is false
> - Added SplitAtLocation and Remove methods to BaseConnection
> - Bugfixes:
> - Fixed an issue where the ItemContainer was selected by releasing the mouse button on it, even when the mouse was not captured
> - Fixed an issue where the ItemContainer could open its context menu even when it was not selected
Expand Down
80 changes: 67 additions & 13 deletions Nodify/Connections/BaseConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,13 @@ protected override Geometry DefiningGeometry
}
}

protected BaseConnection()
{
InputProcessor.AddSharedHandlers(this);
}

#region Drawing

protected abstract ((Point ArrowStartSource, Point ArrowStartTarget), (Point ArrowEndSource, Point ArrowEndTarget)) DrawLineGeometry(StreamGeometryContext context, Point source, Point target);

protected virtual void DrawDirectionalArrowsGeometry(StreamGeometryContext context, Point source, Point target) { }
Expand Down Expand Up @@ -758,6 +765,8 @@ protected virtual Point GetTextPosition(FormattedText text, Point source, Point
return new Point((p0.X + p1.X - text.Width) / 2, (p0.Y + p1.Y - text.Height) / 2);
}

#endregion

/// <summary>Starts animating the directional arrows.</summary>
/// <param name="duration">The duration for moving an arrowhead from <see cref="Source"/> to <see cref="Target"/>.</param>
public void StartAnimation(double duration = 1.5d)
Expand All @@ -772,27 +781,65 @@ public void StopAnimation()
this.CancelAnimation(DirectionalArrowsOffsetProperty);
}

#region Gesture Handling

private InputProcessor InputProcessor { get; } = new InputProcessor();

/// <inheritdoc />
protected override void OnMouseDown(MouseButtonEventArgs e)
=> InputProcessor.Process(e);

/// <inheritdoc />
protected override void OnMouseUp(MouseButtonEventArgs e)
{
Focus();
InputProcessor.Process(e);

EditorGestures.ConnectionGestures gestures = EditorGestures.Mappings.Connection;
if (gestures.Split.Matches(e.Source, e))
// Release the mouse capture if all the mouse buttons are released
if (IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released)
{
Point splitLocation = e.GetPosition(this);
OnSplit(splitLocation);

e.Handled = true;
ReleaseMouseCapture();
}
else if (gestures.Disconnect.Matches(e.Source, e))
{
OnDisconnect();
}

/// <inheritdoc />
protected override void OnMouseMove(MouseEventArgs e)
=> InputProcessor.Process(e);

/// <inheritdoc />
protected override void OnMouseWheel(MouseWheelEventArgs e)
=> InputProcessor.Process(e);

/// <inheritdoc />
protected override void OnLostMouseCapture(MouseEventArgs e)
=> InputProcessor.Process(e);

e.Handled = true;
/// <inheritdoc />
protected override void OnKeyUp(KeyEventArgs e)
{
InputProcessor.Process(e);

// Release the mouse capture if all the mouse buttons are released
if (IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released)
{
ReleaseMouseCapture();
}
}

protected internal void OnSplit(Point splitLocation)
/// <inheritdoc />
protected override void OnKeyDown(KeyEventArgs e)
=> InputProcessor.Process(e);

#endregion

/// <summary>
/// Splits the connection at the specified location.
/// </summary>
/// <param name="splitLocation">The <see cref="Point"/> where the connection should be split.</param>
/// <remarks>
/// This method raises the <see cref="SplitEvent"/> to notify listeners. If the event is not handled,
/// it checks whether the <see cref="SplitCommand"/> can execute with the provided location and executes it if possible.
/// </remarks>
public void SplitAtLocation(Point splitLocation)
{
var args = new ConnectionEventArgs(DataContext)
{
Expand All @@ -810,7 +857,14 @@ protected internal void OnSplit(Point splitLocation)
}
}

protected internal void OnDisconnect()
/// <summary>
/// Removes the connection.
/// </summary>
/// <remarks>
/// This method raises the <see cref="DisconnectEvent"/> to notify listeners. If the event is not handled,
/// it checks whether the <see cref="DisconnectCommand"/> can execute and executes it if possible.
/// </remarks>
public void Remove()
{
var args = new ConnectionEventArgs(DataContext)
{
Expand Down
4 changes: 2 additions & 2 deletions Nodify/Connections/ConnectionContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Nodify
{
internal class ConnectionContainer : ContentPresenter
internal sealed class ConnectionContainer : ContentPresenter
{
#region Dependency properties

Expand Down Expand Up @@ -83,7 +83,7 @@ internal ConnectionContainer(ConnectionsMultiSelector selector)
/// Called when the <see cref="IsSelected"/> value is changed.
/// </summary>
/// <param name="newValue">True if selected, false otherwise.</param>
protected void OnSelectedChanged(bool newValue)
private void OnSelectedChanged(bool newValue)
{
BaseConnection.SetIsSelected(Connection, newValue);

Expand Down
2 changes: 1 addition & 1 deletion Nodify/Connections/ConnectionsMultiSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Nodify
{
internal class ConnectionsMultiSelector : MultiSelector
internal sealed class ConnectionsMultiSelector : MultiSelector
{
public static readonly DependencyProperty SelectedItemsProperty = NodifyEditor.SelectedItemsProperty.AddOwner(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(default(IList), OnSelectedItemsSourceChanged));
public static readonly DependencyProperty CanSelectMultipleItemsProperty = NodifyEditor.CanSelectMultipleItemsProperty.AddOwner(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(BoxValue.True, OnCanSelectMultipleItemsChanged, CoerceCanSelectMultipleItems));
Expand Down
11 changes: 11 additions & 0 deletions Nodify/Connections/States/ConnectionState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Nodify.Interactivity
{
public static partial class ConnectionState
{
internal static void RegisterDefaultHandlers()
{
InputProcessor.Shared<BaseConnection>.RegisterHandlerFactory(elem => new Disconnect(elem));
InputProcessor.Shared<BaseConnection>.RegisterHandlerFactory(elem => new Split(elem));
}
}
}
41 changes: 41 additions & 0 deletions Nodify/Connections/States/Disconnect.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Windows.Input;

namespace Nodify.Interactivity
{
public static partial class ConnectionState
{
/// <summary>
/// Represents a state in which a connection can be disconnected from its connectors based on specific gestures.
/// </summary>
public class Disconnect : InputElementState<BaseConnection>
{
/// <summary>
/// Initializes a new instance of the <see cref="Disconnect"/> class.
/// </summary>
/// <param name="connection">The <see cref="BaseConnection"/> element associated with this state.</param>
public Disconnect(BaseConnection connection) : base(connection)
{
}

protected override void OnMouseDown(MouseButtonEventArgs e)
{
EditorGestures.ConnectionGestures gestures = EditorGestures.Mappings.Connection;
if (gestures.Disconnect.Matches(e.Source, e))
{
Element.Focus();
e.Handled = true; // prevent interacting with the editor
}
}

protected override void OnMouseUp(MouseButtonEventArgs e)
{
EditorGestures.ConnectionGestures gestures = EditorGestures.Mappings.Connection;
if (gestures.Disconnect.Matches(e.Source, e))
{
Element.Remove();
e.Handled = true; // prevent opening context menu
}
}
}
}
}
33 changes: 33 additions & 0 deletions Nodify/Connections/States/Split.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Windows.Input;

namespace Nodify.Interactivity
{
public static partial class ConnectionState
{
/// <summary>
/// Represents a state in which a connection can be split.
/// </summary>
public class Split : InputElementState<BaseConnection>
{
/// <summary>
/// Initializes a new instance of the <see cref="Split"/> class.
/// </summary>
/// <param name="connection">The <see cref="BaseConnection"/> element associated with this state.</param>
public Split(BaseConnection connection) : base(connection)
{
}

protected override void OnMouseDown(MouseButtonEventArgs e)
{
EditorGestures.ConnectionGestures gestures = EditorGestures.Mappings.Connection;
if (gestures.Split.Matches(e.Source, e))
{
Element.Focus();
Element.SplitAtLocation(e.GetPosition(Element));

e.Handled = true; // prevent interacting with the editor
}
}
}
}
}
2 changes: 1 addition & 1 deletion Nodify/Containers/DecoratorsControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Nodify
/// <summary>
/// An <see cref="ItemsControl"/> that works with <see cref="DecoratorContainer"/>s.
/// </summary>
internal class DecoratorsControl : ItemsControl
internal sealed class DecoratorsControl : ItemsControl
{
/// <inheritdoc />
protected override bool IsItemItsOwnContainerOverride(object item)
Expand Down
2 changes: 1 addition & 1 deletion Nodify/Editor/NodifyEditor.Cutting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ private static void RemoveSupportedConnections(List<FrameworkElement> connection
{
if (connection is BaseConnection bc)
{
bc.OnDisconnect();
bc.Remove();
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions Nodify/Interactivity/InputProcessor.Shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ static Shared()
{
MinimapState.RegisterDefaultHandlers();
}
else if(typeof(TElement) == typeof(BaseConnection))
{
ConnectionState.RegisterDefaultHandlers();
}
}

public void HandleEvent(InputEventArgs e)
Expand Down
6 changes: 3 additions & 3 deletions Nodify/Utilities/UnscaleTransformConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Nodify
{
internal class UnscaleTransformConverter : IValueConverter
internal sealed class UnscaleTransformConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Expand All @@ -20,7 +20,7 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu
}
}

internal class ScaleDoubleConverter : IMultiValueConverter
internal sealed class ScaleDoubleConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
Expand All @@ -34,7 +34,7 @@ public object[] ConvertBack(object value, Type[] targetTypes, object parameter,
}
}

internal class ScalePointConverter : IMultiValueConverter
internal sealed class ScalePointConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
Expand Down

0 comments on commit 3786ac7

Please sign in to comment.