Add project files.
This commit is contained in:
112
Nodify/Containers/DecoratorContainer.cs
Normal file
112
Nodify/Containers/DecoratorContainer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
141
Nodify/Containers/DecoratorsControl.cs
Normal file
141
Nodify/Containers/DecoratorsControl.cs
Normal 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
|
||||
}
|
||||
}
|
||||
10
Nodify/Containers/Events/PreviewLocationChanged.cs
Normal file
10
Nodify/Containers/Events/PreviewLocationChanged.cs
Normal 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);
|
||||
}
|
||||
434
Nodify/Containers/ItemContainer.cs
Normal file
434
Nodify/Containers/ItemContainer.cs
Normal 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
|
||||
}
|
||||
}
|
||||
15
Nodify/Containers/States/ContainerState.cs
Normal file
15
Nodify/Containers/States/ContainerState.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
134
Nodify/Containers/States/Default.cs
Normal file
134
Nodify/Containers/States/Default.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Nodify/Containers/States/Dragging.cs
Normal file
43
Nodify/Containers/States/Dragging.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user