Add project files.
This commit is contained in:
28
Nodify/Utilities/BoxValue.cs
Normal file
28
Nodify/Utilities/BoxValue.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
public static class BoxValue
|
||||
{
|
||||
public static readonly object Point = default(Point);
|
||||
public static readonly object Size = default(Size);
|
||||
public static readonly object Rect = default(Rect);
|
||||
public static readonly object False = false;
|
||||
public static readonly object True = true;
|
||||
public static readonly object DoubleHalf = 0.5d;
|
||||
public static readonly object Double0 = 0d;
|
||||
public static readonly object Double1 = 1d;
|
||||
public static readonly object Double2 = 2d;
|
||||
public static readonly object Double5 = 5d;
|
||||
public static readonly object Double45 = 45d;
|
||||
public static readonly object Double1000 = 1000d;
|
||||
public static readonly object Int0 = 0;
|
||||
public static readonly object Int1 = 1;
|
||||
public static readonly object UInt1 = 1u;
|
||||
public static readonly object UInt0 = 0u;
|
||||
|
||||
public static readonly object Thickness2 = new Thickness(2);
|
||||
public static readonly object ArrowSize = new Size(8, 8);
|
||||
public static readonly object ConnectionOffset = new Size(14, 0);
|
||||
}
|
||||
}
|
||||
223
Nodify/Utilities/DependencyObjectExtensions.cs
Normal file
223
Nodify/Utilities/DependencyObjectExtensions.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
internal static class DependencyObjectExtensions
|
||||
{
|
||||
public static T? GetParentOfType<T>(this DependencyObject current)
|
||||
where T : DependencyObject
|
||||
{
|
||||
while ((current = VisualTreeHelper.GetParent(current)) != null)
|
||||
{
|
||||
if (current is T match)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static DependencyObject? GetParent(this DependencyObject current, Func<DependencyObject, bool> condition)
|
||||
{
|
||||
while ((current = VisualTreeHelper.GetParent(current)) != null)
|
||||
{
|
||||
if (condition(current))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static T? GetChildOfType<T>(this DependencyObject? depObj) where T : DependencyObject
|
||||
{
|
||||
if (depObj == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var count = VisualTreeHelper.GetChildrenCount(depObj);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(depObj, i);
|
||||
|
||||
if (child is T result)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (GetChildOfType<T>(child) is T r)
|
||||
{
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public static T? GetElementAtPosition<T>(this UIElement container, Point position)
|
||||
where T : UIElement
|
||||
{
|
||||
T? result = default;
|
||||
VisualTreeHelper.HitTest(container, depObj =>
|
||||
{
|
||||
if (depObj is UIElement elem && elem.IsHitTestVisible)
|
||||
{
|
||||
if (elem is T r)
|
||||
{
|
||||
result = r;
|
||||
return HitTestFilterBehavior.Stop;
|
||||
}
|
||||
|
||||
return HitTestFilterBehavior.Continue;
|
||||
}
|
||||
|
||||
return HitTestFilterBehavior.ContinueSkipSelfAndChildren;
|
||||
}, hitResult =>
|
||||
{
|
||||
if (hitResult.VisualHit is T r)
|
||||
{
|
||||
result = r;
|
||||
return HitTestResultBehavior.Stop;
|
||||
}
|
||||
return HitTestResultBehavior.Continue;
|
||||
}, new PointHitTestParameters(position));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<FrameworkElement> GetIntersectingElements(this UIElement container, Geometry geometry, IReadOnlyCollection<Type> supportedTypes)
|
||||
{
|
||||
var result = new List<FrameworkElement>();
|
||||
VisualTreeHelper.HitTest(container, depObj =>
|
||||
{
|
||||
if (depObj is FrameworkElement elem && elem.IsHitTestVisible)
|
||||
{
|
||||
if (supportedTypes.Contains(elem.GetType()))
|
||||
{
|
||||
return HitTestFilterBehavior.ContinueSkipChildren;
|
||||
}
|
||||
|
||||
return HitTestFilterBehavior.ContinueSkipSelf;
|
||||
}
|
||||
|
||||
return HitTestFilterBehavior.ContinueSkipSelfAndChildren;
|
||||
}, hitResult =>
|
||||
{
|
||||
result.Add((FrameworkElement)hitResult.VisualHit);
|
||||
return HitTestResultBehavior.Continue;
|
||||
}, new GeometryHitTestParameters(geometry));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static IEnumerable<T> GetIntersectingElements<T>(this UIElement container, Rect area, Func<T, Rect> getBounds)
|
||||
where T : Visual
|
||||
{
|
||||
var stack = new Stack<DependencyObject>();
|
||||
stack.Push(container);
|
||||
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
DependencyObject current = stack.Pop();
|
||||
int childrenCount = VisualTreeHelper.GetChildrenCount(current);
|
||||
|
||||
for (int i = 0; i < childrenCount; i++)
|
||||
{
|
||||
DependencyObject child = VisualTreeHelper.GetChild(current, i);
|
||||
|
||||
if (child is T tChild)
|
||||
{
|
||||
var bounds = getBounds(tChild);
|
||||
if (bounds.IntersectsWith(area))
|
||||
{
|
||||
yield return tChild;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
stack.Push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Animation
|
||||
|
||||
public static void StartAnimation(this UIElement animatableElement, DependencyProperty dependencyProperty, Point toValue, double animationDurationSeconds, EventHandler? completedEvent = null)
|
||||
{
|
||||
var fromValue = (Point)animatableElement.GetValue(dependencyProperty);
|
||||
|
||||
PointAnimation animation = new PointAnimation
|
||||
{
|
||||
From = fromValue,
|
||||
To = toValue,
|
||||
Duration = TimeSpan.FromSeconds(animationDurationSeconds)
|
||||
};
|
||||
|
||||
animation.Completed += delegate (object? sender, EventArgs e)
|
||||
{
|
||||
animatableElement.SetValue(dependencyProperty, animatableElement.GetValue(dependencyProperty));
|
||||
CancelAnimation(animatableElement, dependencyProperty);
|
||||
|
||||
completedEvent?.Invoke(sender, e);
|
||||
};
|
||||
|
||||
animation.Freeze();
|
||||
|
||||
animatableElement.BeginAnimation(dependencyProperty, animation);
|
||||
}
|
||||
|
||||
public static void StartAnimation(this UIElement animatableElement, DependencyProperty dependencyProperty, double toValue, double animationDurationSeconds, EventHandler? completedEvent = null)
|
||||
{
|
||||
var fromValue = (double)animatableElement.GetValue(dependencyProperty);
|
||||
|
||||
DoubleAnimation animation = new DoubleAnimation
|
||||
{
|
||||
From = fromValue,
|
||||
To = toValue,
|
||||
Duration = TimeSpan.FromSeconds(animationDurationSeconds)
|
||||
};
|
||||
|
||||
animation.Completed += delegate (object? sender, EventArgs e)
|
||||
{
|
||||
animatableElement.SetValue(dependencyProperty, animatableElement.GetValue(dependencyProperty));
|
||||
CancelAnimation(animatableElement, dependencyProperty);
|
||||
|
||||
completedEvent?.Invoke(sender, e);
|
||||
};
|
||||
|
||||
animation.Freeze();
|
||||
|
||||
animatableElement.BeginAnimation(dependencyProperty, animation);
|
||||
}
|
||||
|
||||
public static void StartLoopingAnimation(this UIElement animatableElement, DependencyProperty dependencyProperty, double toValue, double durationInSeconds)
|
||||
{
|
||||
var fromValue = (double)animatableElement.GetValue(dependencyProperty);
|
||||
|
||||
var animation = new DoubleAnimation
|
||||
{
|
||||
From = fromValue,
|
||||
To = toValue,
|
||||
Duration = TimeSpan.FromSeconds(durationInSeconds)
|
||||
};
|
||||
|
||||
animation.RepeatBehavior = RepeatBehavior.Forever;
|
||||
|
||||
animation.Freeze();
|
||||
animatableElement.BeginAnimation(dependencyProperty, animation);
|
||||
}
|
||||
|
||||
public static void CancelAnimation(this UIElement animatableElement, DependencyProperty dependencyProperty)
|
||||
=> animatableElement.BeginAnimation(dependencyProperty, null);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
79
Nodify/Utilities/EditorGesturesExtensions.cs
Normal file
79
Nodify/Utilities/EditorGesturesExtensions.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using Nodify.Interactivity;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
internal static class EditorGesturesExtensions
|
||||
{
|
||||
public static SelectionType GetSelectionType(this EditorGestures.SelectionGestures gestures, InputEventArgs e)
|
||||
{
|
||||
if (gestures.Append.Matches(e.Source, e))
|
||||
{
|
||||
return SelectionType.Append;
|
||||
}
|
||||
|
||||
if (gestures.Invert.Matches(e.Source, e))
|
||||
{
|
||||
return SelectionType.Invert;
|
||||
}
|
||||
|
||||
if (gestures.Remove.Matches(e.Source, e))
|
||||
{
|
||||
return SelectionType.Remove;
|
||||
}
|
||||
|
||||
return SelectionType.Replace;
|
||||
}
|
||||
|
||||
public static bool TryGetFocusDirection(this EditorGestures.DirectionalNavigationGestures gestures, InputEventArgs e, out FocusNavigationDirection direction)
|
||||
{
|
||||
direction = default;
|
||||
|
||||
if (gestures.Left.Matches(e.Source, e))
|
||||
{
|
||||
direction = FocusNavigationDirection.Left;
|
||||
return true;
|
||||
}
|
||||
if (gestures.Right.Matches(e.Source, e))
|
||||
{
|
||||
direction = FocusNavigationDirection.Right;
|
||||
return true;
|
||||
}
|
||||
if (gestures.Up.Matches(e.Source, e))
|
||||
{
|
||||
direction = FocusNavigationDirection.Up;
|
||||
return true;
|
||||
}
|
||||
if (gestures.Down.Matches(e.Source, e))
|
||||
{
|
||||
direction = FocusNavigationDirection.Down;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetNavigationDirection(this EditorGestures.DirectionalNavigationGestures gestures, InputEventArgs e, out Vector direction)
|
||||
{
|
||||
double y = gestures.Up.Matches(e.Source, e) ? 1 : gestures.Down.Matches(e.Source, e) ? -1 : 0;
|
||||
double x = gestures.Left.Matches(e.Source, e) ? -1 : gestures.Right.Matches(e.Source, e) ? 1 : 0;
|
||||
|
||||
direction = new Vector(x, y);
|
||||
|
||||
return x != 0 || y != 0;
|
||||
}
|
||||
|
||||
public static bool IsOppositeOf(this FocusNavigationDirection direction, FocusNavigationDirection other)
|
||||
{
|
||||
return (direction == FocusNavigationDirection.Left && other == FocusNavigationDirection.Right)
|
||||
|| (direction == FocusNavigationDirection.Right && other == FocusNavigationDirection.Left)
|
||||
|| (direction == FocusNavigationDirection.Up && other == FocusNavigationDirection.Down)
|
||||
|| (direction == FocusNavigationDirection.Down && other == FocusNavigationDirection.Up)
|
||||
|| (direction == FocusNavigationDirection.Next && other == FocusNavigationDirection.Previous)
|
||||
|| (direction == FocusNavigationDirection.Previous && other == FocusNavigationDirection.Next)
|
||||
|| (direction == FocusNavigationDirection.First && other == FocusNavigationDirection.Last)
|
||||
|| (direction == FocusNavigationDirection.Last && other == FocusNavigationDirection.First);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Nodify/Utilities/MathExtensions.cs
Normal file
28
Nodify/Utilities/MathExtensions.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace Nodify
|
||||
{
|
||||
internal static class MathExtensions
|
||||
{
|
||||
/// <summary> Wraps a value within a specified range.</summary>
|
||||
public static double WrapToRange(this double value, double min, double max)
|
||||
{
|
||||
double range = max - min;
|
||||
value = (value - min) % range;
|
||||
|
||||
return value < 0 ? value + range + min : value + min;
|
||||
}
|
||||
|
||||
public static double Clamp(this double value, double min, double max)
|
||||
{
|
||||
if (value < min)
|
||||
{
|
||||
return min;
|
||||
}
|
||||
else if (value > max)
|
||||
{
|
||||
return max;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
193
Nodify/Utilities/SelectionHelper.cs
Normal file
193
Nodify/Utilities/SelectionHelper.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
/// <summary>
|
||||
/// Helps with selecting <see cref="ItemContainer"/>s.
|
||||
/// </summary>
|
||||
internal sealed class SelectionHelper
|
||||
{
|
||||
private Point _startLocation;
|
||||
private Point _endLocation;
|
||||
private bool _isRealtime;
|
||||
private IReadOnlyList<ItemContainer> _items = Array.Empty<ItemContainer>();
|
||||
private IReadOnlyList<ItemContainer> _initialSelection = Array.Empty<ItemContainer>();
|
||||
private Rect _selectedArea;
|
||||
|
||||
public SelectionType Type { get; private set; }
|
||||
|
||||
/// <summary>Attempts to start a new selection.</summary>
|
||||
/// <param name="containers">The containers that can be part of the selection.</param>
|
||||
/// <param name="location">The location inside the graph.</param>
|
||||
/// <param name="selectionType">The type of selection.</param>
|
||||
/// <remarks>Will not do anything if selection is in progress.</remarks>
|
||||
public Rect Start(IEnumerable<ItemContainer> containers, Point location, SelectionType selectionType, bool realtime)
|
||||
{
|
||||
_items = containers.Where(x => x.IsSelectable).ToList();
|
||||
_initialSelection = containers.Where(x => x.IsSelected).ToList();
|
||||
|
||||
Type = selectionType;
|
||||
|
||||
_isRealtime = realtime;
|
||||
_startLocation = location;
|
||||
_endLocation = location;
|
||||
|
||||
_selectedArea = new Rect();
|
||||
return _selectedArea;
|
||||
}
|
||||
|
||||
/// <summary>Update the end location for the selection.</summary>
|
||||
/// <param name="endLocation">An absolute location.</param>
|
||||
public Rect Update(Point endLocation)
|
||||
{
|
||||
_endLocation = endLocation;
|
||||
|
||||
double left = _endLocation.X < _startLocation.X ? _endLocation.X : _startLocation.X;
|
||||
double top = _endLocation.Y < _startLocation.Y ? _endLocation.Y : _startLocation.Y;
|
||||
double width = Math.Abs(_endLocation.X - _startLocation.X);
|
||||
double height = Math.Abs(_endLocation.Y - _startLocation.Y);
|
||||
|
||||
_selectedArea = new Rect(left, top, width, height);
|
||||
|
||||
if (_isRealtime)
|
||||
{
|
||||
PreviewSelection(_selectedArea);
|
||||
}
|
||||
|
||||
return _selectedArea;
|
||||
}
|
||||
|
||||
/// <summary>Increase the selected area by the specified amount.</summary>
|
||||
public Rect Update(Vector amount)
|
||||
{
|
||||
_endLocation += amount;
|
||||
|
||||
return Update(_endLocation);
|
||||
}
|
||||
|
||||
/// <summary>Commits the current selection to the editor.</summary>
|
||||
public Rect End()
|
||||
{
|
||||
PreviewSelection(_selectedArea);
|
||||
_items = Array.Empty<ItemContainer>();
|
||||
_initialSelection = Array.Empty<ItemContainer>();
|
||||
|
||||
return _selectedArea;
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
ClearPreviewingSelection();
|
||||
_items = Array.Empty<ItemContainer>();
|
||||
_initialSelection = Array.Empty<ItemContainer>();
|
||||
}
|
||||
|
||||
#region Selection preview
|
||||
|
||||
private void PreviewSelection(Rect area)
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case SelectionType.Replace:
|
||||
PreviewSelectArea(area);
|
||||
break;
|
||||
|
||||
case SelectionType.Remove:
|
||||
PreviewSelectContainers(_initialSelection);
|
||||
|
||||
PreviewUnselectArea(area);
|
||||
break;
|
||||
|
||||
case SelectionType.Append:
|
||||
PreviewUnselectAll();
|
||||
PreviewSelectContainers(_initialSelection);
|
||||
|
||||
PreviewSelectArea(area, true);
|
||||
break;
|
||||
|
||||
case SelectionType.Invert:
|
||||
PreviewUnselectAll();
|
||||
PreviewSelectContainers(_initialSelection);
|
||||
|
||||
PreviewInvertSelection(area);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotImplementedException(nameof(SelectionType));
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviewUnselectAll()
|
||||
{
|
||||
for (int i = 0; i < _items.Count; i++)
|
||||
{
|
||||
_items[i].IsPreviewingSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviewSelectArea(Rect area, bool append = false, bool fit = false)
|
||||
{
|
||||
if (!append)
|
||||
{
|
||||
PreviewUnselectAll();
|
||||
}
|
||||
|
||||
if (area.X != 0 || area.Y != 0 || area.Width > 0 || area.Height > 0)
|
||||
{
|
||||
for (int i = 0; i < _items.Count; i++)
|
||||
{
|
||||
ItemContainer? container = _items[i];
|
||||
if (container.IsSelectableInArea(area, fit))
|
||||
{
|
||||
container.IsPreviewingSelection = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviewUnselectArea(Rect area, bool fit = false)
|
||||
{
|
||||
for (int i = 0; i < _items.Count; i++)
|
||||
{
|
||||
ItemContainer? container = _items[i];
|
||||
if (container.IsSelectableInArea(area, fit))
|
||||
{
|
||||
container.IsPreviewingSelection = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PreviewSelectContainers(IReadOnlyList<ItemContainer> containers)
|
||||
{
|
||||
for (int i = 0; i < containers.Count; i++)
|
||||
{
|
||||
containers[i].IsPreviewingSelection = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviewInvertSelection(Rect area, bool fit = false)
|
||||
{
|
||||
for (int i = 0; i < _items.Count; i++)
|
||||
{
|
||||
ItemContainer? container = _items[i];
|
||||
if (container.IsSelectableInArea(area, fit))
|
||||
{
|
||||
container.IsPreviewingSelection = !container.IsPreviewingSelection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearPreviewingSelection()
|
||||
{
|
||||
for (int i = 0; i < _items.Count; i++)
|
||||
{
|
||||
_items[i].IsPreviewingSelection = null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
50
Nodify/Utilities/UnscaleTransformConverter.cs
Normal file
50
Nodify/Utilities/UnscaleTransformConverter.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
internal sealed class UnscaleTransformConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
Transform result = (Transform)((TransformGroup)value).Children[0].Inverse;
|
||||
return result;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ScaleDoubleConverter : IMultiValueConverter
|
||||
{
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
double result = (double)values[0] * (double)values[1];
|
||||
return result;
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ScalePointConverter : IMultiValueConverter
|
||||
{
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
Point result = (Point)((Vector)(Point)values[0] * (double)values[1]);
|
||||
return result;
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
132
Nodify/Utilities/WeakReferenceCollection.cs
Normal file
132
Nodify/Utilities/WeakReferenceCollection.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
/// <summary>
|
||||
/// A collection of weak references to objects of type <typeparamref name="T"/>.
|
||||
/// Automatically removes dead references after a configurable number of additions.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The reference type stored in the collection.</typeparam>
|
||||
internal class WeakReferenceCollection<T> : IEnumerable<T> where T : class
|
||||
{
|
||||
private readonly int _cleanupThreshold;
|
||||
private readonly List<WeakReference<T>> _references;
|
||||
|
||||
private int _cleanupCounter = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WeakReferenceCollection{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="initialCapacity">Initial capacity of the internal list.</param>
|
||||
/// <param name="cleanupThreshold">Number of additions after which cleanup is triggered.</param>
|
||||
public WeakReferenceCollection(int initialCapacity, int cleanupThreshold = 32)
|
||||
{
|
||||
_cleanupThreshold = cleanupThreshold;
|
||||
_references = new List<WeakReference<T>>(initialCapacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new weak reference to the specified item to the collection.
|
||||
/// Automatically triggers cleanup after a set number of additions.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to add.</param>
|
||||
public void Add(T item)
|
||||
{
|
||||
_references.Add(new WeakReference<T>(item));
|
||||
_cleanupCounter++;
|
||||
if (_cleanupCounter >= _cleanupThreshold)
|
||||
{
|
||||
_cleanupCounter = 0;
|
||||
Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all weak references from the collection.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
=> _references.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a fast enumerator that iterates only over live references.
|
||||
/// </summary>
|
||||
public IEnumerator<T> GetEnumerator() => new Enumerator(_references);
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Removes dead references from the internal list.
|
||||
/// </summary>
|
||||
private void Cleanup()
|
||||
{
|
||||
int writeIndex = 0;
|
||||
for (int readIndex = 0; readIndex < _references.Count; readIndex++)
|
||||
{
|
||||
var reference = _references[readIndex];
|
||||
if (reference.TryGetTarget(out _))
|
||||
{
|
||||
if (writeIndex != readIndex)
|
||||
_references[writeIndex] = reference;
|
||||
writeIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if (writeIndex < _references.Count)
|
||||
{
|
||||
_references.RemoveRange(writeIndex, _references.Count - writeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Struct-based enumerator for <see cref="WeakReferenceCollection{T}"/>.
|
||||
/// Efficiently enumerates live references only.
|
||||
/// </summary>
|
||||
public struct Enumerator : IEnumerator<T>
|
||||
{
|
||||
private readonly List<WeakReference<T>> _references;
|
||||
private int _index;
|
||||
private T? _current;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Enumerator"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="references">The backing list of weak references.</param>
|
||||
public Enumerator(List<WeakReference<T>> references)
|
||||
{
|
||||
_references = references;
|
||||
_index = -1;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
public T Current => _current!;
|
||||
object IEnumerator.Current => _current!;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
while (++_index < _references.Count)
|
||||
{
|
||||
if (_references[_index].TryGetTarget(out var target))
|
||||
{
|
||||
_current = target;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_current = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_index = -1;
|
||||
_current = null;
|
||||
}
|
||||
|
||||
public readonly void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user