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,112 @@
using Nodify.Interactivity;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace Nodify
{
/// <summary>
/// The container for all the items generated from the <see cref="NodifyEditor.Decorators"/> collection.
/// </summary>
public class DecoratorContainer : ContentControl, INodifyCanvasItem, IKeyboardFocusTarget<DecoratorContainer>
{
#region Dependency Properties
public static readonly DependencyProperty LocationProperty = ItemContainer.LocationProperty.AddOwner(typeof(DecoratorContainer), new FrameworkPropertyMetadata(BoxValue.Point, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.AffectsParentArrange, OnLocationChanged));
public static readonly DependencyProperty ActualSizeProperty = ItemContainer.ActualSizeProperty.AddOwner(typeof(DecoratorContainer));
/// <summary>
/// Gets or sets the location of this <see cref="DecoratorContainer"/> inside the <see cref="NodifyEditor.DecoratorsHost"/>.
/// </summary>
public Point Location
{
get => (Point)GetValue(LocationProperty);
set => SetValue(LocationProperty, value);
}
/// <summary>
/// Gets the actual size of this <see cref="DecoratorContainer"/>.
/// </summary>
public Size ActualSize
{
get => (Size)GetValue(ActualSizeProperty);
set => SetValue(ActualSizeProperty, value);
}
private static void OnLocationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var item = (DecoratorContainer)d;
item.OnLocationChanged();
}
#endregion
#region Routed Events
public static readonly RoutedEvent LocationChangedEvent = EventManager.RegisterRoutedEvent(nameof(LocationChanged), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DecoratorContainer));
/// <summary>
/// Occurs when the <see cref="Location"/> of this <see cref="DecoratorContainer"/> is changed.
/// </summary>
public event RoutedEventHandler LocationChanged
{
add => AddHandler(LocationChangedEvent, value);
remove => RemoveHandler(LocationChangedEvent, value);
}
/// <summary>
/// Raises the <see cref="LocationChangedEvent"/>.
/// </summary>
protected void OnLocationChanged()
{
RaiseEvent(new RoutedEventArgs(LocationChangedEvent, this));
}
#endregion
public Rect Bounds => new Rect(Location, ActualSize);
DecoratorContainer IKeyboardFocusTarget<DecoratorContainer>.Element => this;
private DecoratorsControl? _owner;
public DecoratorsControl? Owner => _owner ??= this.GetParentOfType<DecoratorsControl>();
static DecoratorContainer()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DecoratorContainer), new FrameworkPropertyMetadata(typeof(DecoratorContainer)));
FocusableProperty.OverrideMetadata(typeof(DecoratorContainer), new FrameworkPropertyMetadata(BoxValue.True));
KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(DecoratorContainer), new FrameworkPropertyMetadata(KeyboardNavigationMode.Cycle));
KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(DecoratorContainer), new FrameworkPropertyMetadata(KeyboardNavigationMode.Cycle));
}
public DecoratorContainer(DecoratorsControl parent)
{
_owner = parent;
}
public DecoratorContainer()
{
}
/// <inheritdoc />
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
SetCurrentValue(ActualSizeProperty, sizeInfo.NewSize);
}
protected override void OnVisualParentChanged(DependencyObject oldParent)
{
if (VisualTreeHelper.GetParent(this) == null && IsKeyboardFocusWithin)
{
base.OnVisualParentChanged(oldParent);
Owner?.Editor?.Focus();
}
else
{
base.OnVisualParentChanged(oldParent);
}
}
}
}

View File

@@ -0,0 +1,141 @@
using Nodify.Interactivity;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Nodify
{
/// <summary>
/// An <see cref="ItemsControl"/> that works with <see cref="DecoratorContainer"/>s.
/// </summary>
public class DecoratorsControl : ItemsControl, IKeyboardNavigationLayer
{
/// <summary>
/// Gets the <see cref="NodifyEditor"/> that owns this <see cref="DecoratorsControl"/>.
/// </summary>
public NodifyEditor? Editor { get; private set; }
/// <summary>
/// Gets a list of all <see cref="DecoratorContainer"/>s.
/// </summary>
/// <remarks>Cache the result before using it to avoid extra allocations.</remarks>
protected internal IReadOnlyCollection<DecoratorContainer> DecoratorContainers
{
get
{
ItemCollection items = Items;
var containers = new List<DecoratorContainer>(items.Count);
for (var i = 0; i < items.Count; i++)
{
containers.Add((DecoratorContainer)ItemContainerGenerator.ContainerFromIndex(i));
}
return containers;
}
}
static DecoratorsControl()
{
FocusableProperty.OverrideMetadata(typeof(DecoratorsControl), new FrameworkPropertyMetadata(BoxValue.False));
KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(DecoratorsControl), new FrameworkPropertyMetadata(KeyboardNavigationMode.None));
KeyboardNavigation.ControlTabNavigationProperty.OverrideMetadata(typeof(DecoratorsControl), new FrameworkPropertyMetadata(KeyboardNavigationMode.None));
KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(DecoratorsControl), new FrameworkPropertyMetadata(KeyboardNavigationMode.None));
}
public DecoratorsControl()
{
_focusNavigator = new StatefulFocusNavigator<DecoratorContainer>(OnElementFocused);
}
/// <inheritdoc />
protected override bool IsItemItsOwnContainerOverride(object item)
=> item is DecoratorContainer;
/// <inheritdoc />
protected override DependencyObject GetContainerForItemOverride()
=> new DecoratorContainer(this);
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Editor = this.GetParentOfType<NodifyEditor>();
if (NodifyEditor.AutoRegisterDecoratorsLayer)
{
Editor?.RegisterNavigationLayer(this);
}
}
#region Keyboard Navigation
public KeyboardNavigationLayerId Id { get; } = KeyboardNavigationLayerId.Decorators;
public IKeyboardFocusTarget<UIElement>? LastFocusedElement => _focusNavigator.LastFocusedElement;
private readonly StatefulFocusNavigator<DecoratorContainer> _focusNavigator;
public bool TryMoveFocus(TraversalRequest request)
{
return _focusNavigator.TryMoveFocus(request, TryFindContainerToFocus);
}
public bool TryRestoreFocus()
{
return _focusNavigator.TryRestoreFocus();
}
private bool TryFindContainerToFocus(DecoratorContainer? currentElement, TraversalRequest request, out DecoratorContainer? containerToFocus)
{
containerToFocus = null;
if (currentElement is DecoratorContainer focusedContainer)
{
containerToFocus = FindNextFocusTarget(focusedContainer, request);
}
else if (currentElement is UIElement elem && elem.GetParentOfType<DecoratorContainer>() is DecoratorContainer parentContainer)
{
containerToFocus = parentContainer;
}
else if (Items.Count > 0 && Editor != null)
{
var viewport = new Rect(Editor.ViewportLocation, Editor.ViewportSize);
var containers = DecoratorContainers;
containerToFocus = containers.FirstOrDefault(container => viewport.IntersectsWith(((IKeyboardFocusTarget<DecoratorContainer>)container).Bounds))
?? containers.First();
}
return containerToFocus != null;
}
protected virtual DecoratorContainer? FindNextFocusTarget(DecoratorContainer currentContainer, TraversalRequest request)
{
var focusNavigator = new DirectionalFocusNavigator<DecoratorContainer>(DecoratorContainers);
var result = focusNavigator.FindNextFocusTarget(currentContainer, request);
return result?.Element;
}
protected virtual void OnElementFocused(IKeyboardFocusTarget<DecoratorContainer> target)
{
if (NodifyEditor.AutoPanOnNodeFocus)
{
Editor?.BringIntoView(target.Bounds, NodifyEditor.BringIntoViewEdgeOffset);
}
}
void IKeyboardNavigationLayer.OnActivated()
{
TryRestoreFocus();
}
void IKeyboardNavigationLayer.OnDeactivated()
{
}
#endregion
}
}

View File

@@ -0,0 +1,10 @@
using System.Windows;
namespace Nodify.Events
{
/// <summary>
/// Delegate used to notify when an <see cref="ItemContainer"/> is previewing a new location.
/// </summary>
/// <param name="newLocation">The new location.</param>
public delegate void PreviewLocationChanged(Point newLocation);
}

View File

@@ -0,0 +1,434 @@
using Nodify.Events;
using Nodify.Interactivity;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
namespace Nodify
{
/// <summary>
/// The container for all the items generated by the <see cref="ItemsControl.ItemsSource"/> of the <see cref="NodifyEditor"/>.
/// </summary>
public class ItemContainer : ContentControl, INodifyCanvasItem, IKeyboardFocusTarget<ItemContainer>
{
#region Dependency Properties
public static readonly DependencyProperty HighlightBrushProperty = DependencyProperty.Register(nameof(HighlightBrush), typeof(Brush), typeof(ItemContainer));
public static readonly DependencyProperty SelectedBrushProperty = DependencyProperty.Register(nameof(SelectedBrush), typeof(Brush), typeof(ItemContainer));
public static readonly DependencyProperty SelectedBorderThicknessProperty = DependencyProperty.Register(nameof(SelectedBorderThickness), typeof(Thickness), typeof(ItemContainer), new FrameworkPropertyMetadata(BoxValue.Thickness2));
public static readonly DependencyProperty IsSelectableProperty = DependencyProperty.Register(nameof(IsSelectable), typeof(bool), typeof(ItemContainer), new FrameworkPropertyMetadata(BoxValue.True));
public static readonly DependencyProperty IsSelectedProperty = Selector.IsSelectedProperty.AddOwner(typeof(ItemContainer), new FrameworkPropertyMetadata(BoxValue.False, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnIsSelectedChanged));
protected static readonly DependencyPropertyKey IsPreviewingSelectionPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsPreviewingSelection), typeof(bool?), typeof(ItemContainer), new FrameworkPropertyMetadata(null));
public static readonly DependencyProperty IsPreviewingSelectionProperty = IsPreviewingSelectionPropertyKey.DependencyProperty;
public static readonly DependencyProperty LocationProperty = DependencyProperty.Register(nameof(Location), typeof(Point), typeof(ItemContainer), new FrameworkPropertyMetadata(BoxValue.Point, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnLocationChanged));
public static readonly DependencyProperty ActualSizeProperty = DependencyProperty.Register(nameof(ActualSize), typeof(Size), typeof(ItemContainer), new FrameworkPropertyMetadata(BoxValue.Size));
public static readonly DependencyProperty DesiredSizeForSelectionProperty = DependencyProperty.Register(nameof(DesiredSizeForSelection), typeof(Size?), typeof(ItemContainer), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.NotDataBindable));
private static readonly DependencyPropertyKey IsPreviewingLocationPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsPreviewingLocation), typeof(bool), typeof(ItemContainer), new FrameworkPropertyMetadata(BoxValue.False));
public static readonly DependencyProperty IsPreviewingLocationProperty = IsPreviewingLocationPropertyKey.DependencyProperty;
public static readonly DependencyProperty IsDraggableProperty = DependencyProperty.Register(nameof(IsDraggable), typeof(bool), typeof(ItemContainer), new FrameworkPropertyMetadata(BoxValue.True));
public static readonly DependencyProperty HasCustomContextMenuProperty = NodifyEditor.HasCustomContextMenuProperty.AddOwner(typeof(ItemContainer));
private static void OnLocationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var item = (ItemContainer)d;
item.OnLocationChanged();
if (item.Editor.IsLoaded && !item.Editor.IsBulkUpdatingItems)
{
item.Editor.ItemsHost.InvalidateArrange();
}
}
/// <summary>
/// Gets or sets the brush used when the <see cref="PendingConnection.IsOverElementProperty"/> attached property is true for this <see cref="ItemContainer"/>.
/// </summary>
public Brush HighlightBrush
{
get => (Brush)GetValue(HighlightBrushProperty);
set => SetValue(HighlightBrushProperty, value);
}
/// <summary>
/// Gets or sets the brush used when <see cref="IsSelected"/> or <see cref="IsPreviewingSelection"/> is true.
/// </summary>
public Brush SelectedBrush
{
get => (Brush)GetValue(SelectedBrushProperty);
set => SetValue(SelectedBrushProperty, value);
}
/// <summary>
/// Gets or sets the border thickness used when <see cref="IsSelected"/> or <see cref="IsPreviewingSelection"/> is true.
/// </summary>
public Thickness SelectedBorderThickness
{
get => (Thickness)GetValue(SelectedBorderThicknessProperty);
set => SetValue(SelectedBorderThicknessProperty, value);
}
/// <summary>
/// Gets or sets the location of this <see cref="ItemContainer"/> inside the <see cref="NodifyEditor"/> in graph space coordinates.
/// </summary>
public Point Location
{
get => (Point)GetValue(LocationProperty);
set => SetValue(LocationProperty, value);
}
/// <summary>
/// Gets or sets a value that indicates whether this <see cref="ItemContainer"/> is selected.
/// Can only be set if <see cref="IsSelectable"/> is true.
/// </summary>
public bool IsSelected
{
get => (bool)GetValue(IsSelectedProperty);
set => SetValue(IsSelectedProperty, value);
}
/// <summary>
/// Gets a value indicating whether this <see cref="ItemContainer"/> is about to change its <see cref="IsSelected"/> state.
/// </summary>
public bool? IsPreviewingSelection
{
get => (bool?)GetValue(IsPreviewingSelectionProperty);
internal set => SetValue(IsPreviewingSelectionPropertyKey, value);
}
/// <summary>
/// Gets or sets whether this <see cref="ItemContainer"/> can be selected.
/// </summary>
public bool IsSelectable
{
get => (bool)GetValue(IsSelectableProperty);
set => SetValue(IsSelectableProperty, value);
}
/// <summary>
/// Gets a value indicating whether this <see cref="ItemContainer"/> is previewing a new location but didn't logically move there.
/// </summary>
public bool IsPreviewingLocation
{
get => (bool)GetValue(IsPreviewingLocationProperty);
protected internal set => SetValue(IsPreviewingLocationPropertyKey, value);
}
/// <summary>
/// Gets the actual size of this <see cref="ItemContainer"/>.
/// </summary>
public Size ActualSize
{
get => (Size)GetValue(ActualSizeProperty);
set => SetValue(ActualSizeProperty, value);
}
/// <summary>
/// Overrides the size to check against when calculating if this <see cref="ItemContainer"/> can be part of the current <see cref="NodifyEditor.SelectedArea"/>.
/// Defaults to <see cref="UIElement.RenderSize"/>.
/// </summary>
public Size? DesiredSizeForSelection
{
get => (Size?)GetValue(DesiredSizeForSelectionProperty);
set => SetValue(DesiredSizeForSelectionProperty, value);
}
/// <summary>
/// Gets or sets whether this <see cref="ItemContainer"/> can be dragged.
/// </summary>
public bool IsDraggable
{
get => (bool)GetValue(IsDraggableProperty);
set => SetValue(IsDraggableProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the container uses a custom context menu.
/// </summary>
/// <remarks>When set to true, the container 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 container has a context menu.
/// </summary>
public bool HasContextMenu => ContextMenu != null || HasCustomContextMenu;
#endregion
#region Routed Events
public static readonly RoutedEvent SelectedEvent = Selector.SelectedEvent.AddOwner(typeof(ItemContainer));
public static readonly RoutedEvent UnselectedEvent = Selector.UnselectedEvent.AddOwner(typeof(ItemContainer));
public static readonly RoutedEvent LocationChangedEvent = EventManager.RegisterRoutedEvent(nameof(LocationChanged), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ItemContainer));
/// <summary>
/// Occurs when the <see cref="Location"/> of this <see cref="ItemContainer"/> is changed.
/// </summary>
public event RoutedEventHandler LocationChanged
{
add => AddHandler(LocationChangedEvent, value);
remove => RemoveHandler(LocationChangedEvent, value);
}
/// <summary>
/// Occurs when this <see cref="ItemContainer"/> is selected.
/// </summary>
public event RoutedEventHandler Selected
{
add => AddHandler(SelectedEvent, value);
remove => RemoveHandler(SelectedEvent, value);
}
/// <summary>
/// Occurs when this <see cref="ItemContainer"/> is unselected.
/// </summary>
public event RoutedEventHandler Unselected
{
add => AddHandler(UnselectedEvent, value);
remove => RemoveHandler(UnselectedEvent, value);
}
/// <summary>
/// Raises the <see cref="LocationChangedEvent"/> and sets <see cref="IsPreviewingLocation"/> to false.
/// </summary>
protected void OnLocationChanged()
{
IsPreviewingLocation = false;
RaiseEvent(new RoutedEventArgs(LocationChangedEvent, this));
}
/// <summary>
/// Raises the <see cref="SelectedEvent"/> or <see cref="UnselectedEvent"/> based on <paramref name="newValue"/>.
/// Called when the <see cref="IsSelected"/> value is changed.
/// </summary>
/// <param name="newValue">True if selected, false otherwise.</param>
protected void OnSelectedChanged(bool newValue)
{
// Don't raise the event if the editor is selecting
if (!Editor.IsSelecting)
{
RaiseEvent(new RoutedEventArgs(newValue ? SelectedEvent : UnselectedEvent, this));
}
}
private static void OnIsSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var elem = (ItemContainer)d;
bool result = elem.IsSelectable && (bool)e.NewValue;
elem.IsSelected = result;
elem.OnSelectedChanged(result);
}
#endregion
#region Fields
/// <summary>
/// Indicates whether right-click on the container should preserve the current selection.
/// </summary>
/// <remarks>Has no effect if the container has a context menu.</remarks>
public static bool PreserveSelectionOnRightClick { get; set; }
/// <summary>
/// The <see cref="NodifyEditor"/> that owns this <see cref="ItemContainer"/>.
/// </summary>
public NodifyEditor Editor { get; }
/// <summary>
/// The calculated margin when the container is selected or previewing selection.
/// </summary>
public Thickness SelectedMargin => new Thickness(
BorderThickness.Left - SelectedBorderThickness.Left,
BorderThickness.Top - SelectedBorderThickness.Top,
BorderThickness.Right - SelectedBorderThickness.Right,
BorderThickness.Bottom - SelectedBorderThickness.Bottom);
/// <summary>
/// Gets the bounds of the selection area for this <see cref="ItemContainer"/> based on its <see cref="Location"/> and <see cref="DesiredSizeForSelection"/>.
/// </summary>
public Rect Bounds => new Rect(Location, DesiredSizeForSelection ?? RenderSize);
ItemContainer IKeyboardFocusTarget<ItemContainer>.Element => this;
#endregion
/// <summary>
/// Occurs when the <see cref="ItemContainer"/> is previewing a new location.
/// </summary>
public event PreviewLocationChanged? PreviewLocationChanged;
/// <summary>
/// Raises the <see cref="PreviewLocationChanged"/> event and sets the <see cref="IsPreviewingLocation"/> property to true.
/// </summary>
/// <param name="newLocation">The new location.</param>
protected internal void OnPreviewLocationChanged(Point newLocation)
{
IsPreviewingLocation = true;
PreviewLocationChanged?.Invoke(newLocation);
}
static ItemContainer()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(typeof(ItemContainer)));
FocusableProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(BoxValue.True));
KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(KeyboardNavigationMode.Cycle));
KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(KeyboardNavigationMode.Cycle));
}
/// <summary>
/// Constructs an instance of an <see cref="ItemContainer"/> in the specified <see cref="NodifyEditor"/>.
/// </summary>
/// <param name="editor"></param>
public ItemContainer(NodifyEditor editor)
{
Editor = editor;
InputProcessor.AddSharedHandlers(this);
}
protected override void OnVisualParentChanged(DependencyObject oldParent)
{
if (VisualTreeHelper.GetParent(this) == null && IsKeyboardFocusWithin)
{
base.OnVisualParentChanged(oldParent);
Editor.Focus();
}
else
{
base.OnVisualParentChanged(oldParent);
}
}
/// <inheritdoc />
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
ActualSize = sizeInfo.NewSize;
base.OnRenderSizeChanged(sizeInfo);
}
/// <summary>
/// Checks if <paramref name="position"/> is selectable.
/// </summary>
/// <param name="position">A position relative to this <see cref="ItemContainer"/>.</param>
/// <returns>True if <paramref name="position"/> is selectable.</returns>
protected internal virtual bool IsSelectableLocation(Point position)
{
Size size = Bounds.Size;
return position.X >= 0 && position.Y >= 0 && position.X <= size.Width && position.Y <= size.Height;
}
/// <summary>
/// Checks if <paramref name="area"/> contains or intersects with this <see cref="ItemContainer"/> taking into consideration the <see cref="DesiredSizeForSelection"/>.
/// </summary>
/// <param name="area">The area to check if contains or intersects this <see cref="ItemContainer"/>.</param>
/// <param name="isContained">If true will check if <paramref name="area"/> contains this, otherwise will check if <paramref name="area"/> intersects with this.</param>
/// <returns>True if <paramref name="area"/> contains or intersects this <see cref="ItemContainer"/>.</returns>
public virtual bool IsSelectableInArea(Rect area, bool isContained)
{
return isContained ? area.Contains(Bounds) : area.IntersectsWith(Bounds);
}
/// <inheritdoc cref="NodifyEditor.BeginDragging()" />
/// <remarks>Replaces the selection if the container is not selected.</remarks>
public void BeginDragging()
{
if (!IsSelected)
{
Select(SelectionType.Replace);
}
Editor.BeginDragging();
}
/// <inheritdoc cref="NodifyEditor.UpdateDragging(Vector)" />
public void UpdateDragging(Vector amount)
=> Editor.UpdateDragging(amount);
/// <inheritdoc cref="NodifyEditor.CancelDragging" />
public void CancelDragging()
=> Editor.CancelDragging();
/// <inheritdoc cref="NodifyEditor.EndDragging" />
public void EndDragging()
=> Editor.EndDragging();
/// <summary>
/// Modifies the selection state of the current item based on the specified selection type.
/// </summary>
/// <param name="type">The type of selection to perform.</param>
public void Select(SelectionType type)
{
switch (type)
{
case SelectionType.Append:
IsSelected = true;
break;
case SelectionType.Remove:
IsSelected = false;
break;
case SelectionType.Invert:
IsSelected = !IsSelected;
break;
case SelectionType.Replace:
Editor.Select(this);
break;
}
}
#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
}
}

View File

@@ -0,0 +1,15 @@
namespace Nodify.Interactivity
{
public static partial class ContainerState
{
/// <summary>
/// Determines whether toggled dragging mode is enabled, allowing the user to start and end the interaction in two steps with the same input gesture.
/// </summary>
public static bool EnableToggledDraggingMode { get; set; }
internal static void RegisterDefaultHandlers()
{
InputProcessor.Shared<ItemContainer>.RegisterHandlerFactory(elem => new Default(elem));
}
}
}

View File

@@ -0,0 +1,134 @@
using System.Windows;
using System.Windows.Input;
namespace Nodify.Interactivity
{
public static partial class ContainerState
{
/// <summary>The default state of the <see cref="ItemContainer"/>.</summary>
public sealed class Default : InputElementStateStack<ItemContainer>
{
public Default(ItemContainer container) : base(container)
{
PushState(new SelectingState(this));
}
private sealed class SelectingState : InputElementState
{
private Point _initialPosition;
private SelectionType? _selectionType;
private bool _isDragging;
private bool PreserveSelectionOnRightClick => Element.HasContextMenu || ItemContainer.PreserveSelectionOnRightClick;
/// <summary>Creates a new instance of the <see cref="ContainerSelectingState"/>.</summary>
/// <param name="container">The owner of the state.</param>
public SelectingState(InputElementStateStack<ItemContainer> stack) : base(stack)
{
}
/// <inheritdoc />
public override void Enter(IInputElementState? from)
{
_isDragging = false;
_selectionType = null;
_initialPosition = Element.Editor.MouseLocation;
}
protected override void OnMouseDown(MouseButtonEventArgs e)
{
if (!Element.IsSelectableLocation(e.GetPosition(Element)))
{
return;
}
EditorGestures.ItemContainerGestures gestures = EditorGestures.Mappings.ItemContainer;
if (gestures.Drag.Matches(e.Source, e))
{
// Dragging requires mouse capture
_isDragging = Element.IsDraggable && CanCaptureMouse();
}
if (gestures.Selection.Select.Matches(e.Source, e))
{
_selectionType = gestures.Selection.GetSelectionType(e);
}
// Replaces the current selection when right-clicking on an element that has a context menu and is not selected.
// Applies only when the select gesture is not right click.
else if (e.ChangedButton == MouseButton.Right && PreserveSelectionOnRightClick)
{
_selectionType = Element.IsSelected ? SelectionType.Append : SelectionType.Replace;
}
_initialPosition = Element.Editor.MouseLocation;
if (_isDragging || _selectionType.HasValue)
{
e.Handled = true;
CaptureMouseSafe();
}
}
/// <inheritdoc />
protected override void OnMouseMove(MouseEventArgs e)
{
double dragThreshold = NodifyEditor.MouseActionSuppressionThreshold * NodifyEditor.MouseActionSuppressionThreshold;
double dragDistance = (Element.Editor.MouseLocation - _initialPosition).LengthSquared;
if (_isDragging && (dragDistance > dragThreshold))
{
if (!Element.IsSelected)
{
var selectionType = GetSelectionTypeForDragging(_selectionType);
Element.Select(selectionType);
}
PushState(new Dragging(Stack));
}
}
/// <inheritdoc />
protected override void OnMouseUp(MouseButtonEventArgs e)
{
if (_selectionType.HasValue)
{
// Determine whether the current selection should remain intact or be replaced by the clicked item.
// If the right mouse button is pressed on an already selected item, and the item either has an
// explicit context menu or is configured to preserve the selection on right-click, the selection
// remains unchanged. This ensures that the context menu applies to the entire selection rather
// than only the clicked item.
bool allowContextMenu = e.ChangedButton == MouseButton.Right && Element.IsSelected && PreserveSelectionOnRightClick;
if (!allowContextMenu)
{
Element.Select(_selectionType.Value);
}
}
_isDragging = false;
_selectionType = null;
}
private void CaptureMouseSafe()
{
// Avoid stealing mouse capture from other elements
if (CanCaptureMouse())
{
Element.Focus();
Element.CaptureMouse();
}
}
private bool CanCaptureMouse()
=> Mouse.Captured == null || Element.IsMouseCaptured;
private static SelectionType GetSelectionTypeForDragging(SelectionType? selectionType)
{
// Always select the container when dragging
return selectionType == SelectionType.Remove
? SelectionType.Replace
: selectionType.GetValueOrDefault(SelectionType.Replace);
}
}
}
}
}

View File

@@ -0,0 +1,43 @@
using System.Windows;
using System.Windows.Input;
namespace Nodify.Interactivity
{
public static partial class ContainerState
{
/// <summary>Dragging state of the container.</summary>
internal sealed class Dragging : InputElementStateStack<ItemContainer>.DragState
{
protected override bool CanCancel => NodifyEditor.AllowDraggingCancellation;
protected override bool IsToggle => EnableToggledDraggingMode;
private Point _previousMousePosition;
/// <summary>Constructs an instance of the <see cref="Dragging"/> state.</summary>
/// <param name="container">The owner of the state.</param>
public Dragging(InputElementStateStack<ItemContainer> stack)
: base(stack, EditorGestures.Mappings.ItemContainer.Drag, EditorGestures.Mappings.ItemContainer.CancelAction)
{
PositionElement = Element.Editor;
}
protected override void OnBegin(InputEventArgs e)
{
_previousMousePosition = Element.Editor.MouseLocation;
Element.BeginDragging();
}
protected override void OnMouseMove(MouseEventArgs e)
{
Element.UpdateDragging(Element.Editor.MouseLocation - _previousMousePosition);
_previousMousePosition = Element.Editor.MouseLocation;
}
protected override void OnEnd(InputEventArgs e)
=> Element.EndDragging();
protected override void OnCancel(InputEventArgs e)
=> Element.CancelDragging();
}
}
}