diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d20dc2c..b6d48668 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,11 @@ > - Made the setter of NodifyEditor.IsPanning private > - Renamed StartCutting to BeginCutting in NodifyEditor > - Renamed PushItems to UpdatePushedArea and StartPushingItems to BeginPushingItems in NodifyEditor +> - Renamed UnselectAllConnection to UnselectAllConnections in NodifyEditor > - Features: > - Added BeginPanning, UpdatePanning, EndPanning, CancelPanning and AllowPanningCancellation to NodifyEditor > - Added UpdateCuttingLine to NodifyEditor +> - Added BeginSelecting, UpdateSelection, EndSelecting, CancelSelecting and AllowSelectionCancellation to NodifyEditor > - Bugfixes: #### **Version 6.6.0** diff --git a/Nodify/EditorStates/EditorCuttingState.cs b/Nodify/EditorStates/EditorCuttingState.cs index 2bef1545..b9d6261e 100644 --- a/Nodify/EditorStates/EditorCuttingState.cs +++ b/Nodify/EditorStates/EditorCuttingState.cs @@ -4,7 +4,7 @@ namespace Nodify { public class EditorCuttingState : EditorState { - public bool Canceled { get; set; } = CuttingLine.AllowCuttingCancellation; + private bool Canceled { get; set; } = CuttingLine.AllowCuttingCancellation; public EditorCuttingState(NodifyEditor editor) : base(editor) { diff --git a/Nodify/EditorStates/EditorDefaultState.cs b/Nodify/EditorStates/EditorDefaultState.cs index bc2b35e1..eba8eed1 100644 --- a/Nodify/EditorStates/EditorDefaultState.cs +++ b/Nodify/EditorStates/EditorDefaultState.cs @@ -1,6 +1,5 @@ using System.Windows; using System.Windows.Input; -using static Nodify.SelectionHelper; namespace Nodify { @@ -32,23 +31,22 @@ public EditorDefaultState(NodifyEditor editor) : base(editor) public override void HandleMouseDown(MouseButtonEventArgs e) { EditorGestures.NodifyEditorGestures gestures = EditorGestures.Mappings.Editor; - if (gestures.Cutting.Matches(e.Source, e)) + if (Editor.CanSelectMultipleItems && gestures.Selection.Select.Matches(e.Source, e)) { - PushState(new EditorCuttingState(Editor)); + SelectionType selectionType = SelectionHelper.GetSelectionType(e); + PushState(new EditorSelectingState(Editor, selectionType)); } - else if (gestures.PushItems.Matches(e.Source, e)) + else if (!Editor.DisablePanning && gestures.Pan.Matches(e.Source, e)) { - PushState(new EditorPushingItemsState(Editor)); + PushState(new EditorPanningState(Editor)); } - else if (gestures.Selection.Select.Matches(e.Source, e)) + else if (gestures.Cutting.Matches(e.Source, e)) { - SelectionType selectionType = GetSelectionType(e); - var selecting = new EditorSelectingState(Editor, selectionType); - PushState(selecting); + PushState(new EditorCuttingState(Editor)); } - else if (!Editor.DisablePanning && gestures.Pan.Matches(e.Source, e)) + else if (gestures.PushItems.Matches(e.Source, e)) { - PushState(new EditorPanningState(Editor)); + PushState(new EditorPushingItemsState(Editor)); } } diff --git a/Nodify/EditorStates/EditorPushingItemsState.cs b/Nodify/EditorStates/EditorPushingItemsState.cs index 99441115..fb69c3ea 100644 --- a/Nodify/EditorStates/EditorPushingItemsState.cs +++ b/Nodify/EditorStates/EditorPushingItemsState.cs @@ -10,7 +10,7 @@ public class EditorPushingItemsState : EditorState private Point _prevPosition; private const int _minDragDistance = 10; - public bool Canceled { get; set; } = NodifyEditor.AllowPushItemsCancellation; + private bool Canceled { get; set; } = NodifyEditor.AllowPushItemsCancellation; public EditorPushingItemsState(NodifyEditor editor) : base(editor) { diff --git a/Nodify/EditorStates/EditorSelectingState.cs b/Nodify/EditorStates/EditorSelectingState.cs index dd878091..d1f8c54d 100644 --- a/Nodify/EditorStates/EditorSelectingState.cs +++ b/Nodify/EditorStates/EditorSelectingState.cs @@ -1,5 +1,4 @@ using System.Windows.Input; -using static Nodify.SelectionHelper; namespace Nodify { @@ -7,45 +6,41 @@ namespace Nodify public class EditorSelectingState : EditorState { private readonly SelectionType _type; - private bool _canceled; - - /// The selection helper. - protected SelectionHelper Selection { get; } + private bool Canceled { get; set; } = NodifyEditor.AllowSelectionCancellation; /// Constructs an instance of the state. /// The owner of the state. /// The selection strategy. public EditorSelectingState(NodifyEditor editor, SelectionType type) : base(editor) { - Selection = new SelectionHelper(editor); _type = type; } /// public override void Enter(EditorState? from) { - Editor.UnselectAllConnection(); + Canceled = false; - _canceled = false; - Selection.Start(Editor.MouseLocation, _type); + Editor.BeginSelecting(Editor.MouseLocation, _type); } /// public override void Exit() { - if (_canceled) + // TODO: This is not canceled on LostMouseCapture (add OnLostMouseCapture/OnCancel callback?) + if (Canceled) { - Selection.Abort(); + Editor.CancelSelecting(); } else { - Selection.End(); + Editor.EndSelecting(); } } /// - public override void HandleMouseMove(MouseEventArgs e) - => Selection.Update(Editor.MouseLocation); + public override void HandleMouseMove(MouseEventArgs e) + => Editor.UpdateSelection(Editor.MouseLocation); /// public override void HandleMouseDown(MouseButtonEventArgs e) @@ -60,12 +55,15 @@ public override void HandleMouseDown(MouseButtonEventArgs e) public override void HandleMouseUp(MouseButtonEventArgs e) { EditorGestures.SelectionGestures gestures = EditorGestures.Mappings.Editor.Selection; - - bool canCancel = gestures.Cancel.Matches(e.Source, e); - bool canComplete = gestures.Select.Matches(e.Source, e); - if (canCancel || canComplete) + if(gestures.Select.Matches(e.Source, e)) { - _canceled = !canComplete && canCancel; + PopState(); + } + else if(NodifyEditor.AllowSelectionCancellation && gestures.Cancel.Matches(e.Source, e)) + { + Canceled = true; + e.Handled = true; // prevents opening context menu + PopState(); } } @@ -76,9 +74,10 @@ public override void HandleAutoPanning(MouseEventArgs e) public override void HandleKeyUp(KeyEventArgs e) { - if (EditorGestures.Mappings.Editor.Selection.Cancel.Matches(e.Source, e)) + EditorGestures.SelectionGestures gestures = EditorGestures.Mappings.Editor.Selection; + if (NodifyEditor.AllowSelectionCancellation && gestures.Cancel.Matches(e.Source, e)) { - _canceled = true; + Canceled = true; PopState(); } } diff --git a/Nodify/Helpers/SelectionHelper.cs b/Nodify/Helpers/SelectionHelper.cs index 2f4869bd..699a90fd 100644 --- a/Nodify/Helpers/SelectionHelper.cs +++ b/Nodify/Helpers/SelectionHelper.cs @@ -1,96 +1,81 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Windows; -using System.Windows.Controls; using System.Windows.Input; namespace Nodify { /// - /// Helps with selecting s and updating the and properties. + /// Helps with selecting s. /// - public sealed class SelectionHelper + internal sealed class SelectionHelper { - private readonly NodifyEditor _host; private Point _startLocation; + private Point _endLocation; private SelectionType _selectionType; private bool _isRealtime; - private IReadOnlyList _initialSelection = new List(); - - /// Constructs a new instance of a . - /// The editor to select items from. - public SelectionHelper(NodifyEditor host) - => _host = host; - - /// Available selection logic. - public enum SelectionType - { - /// Replaces the old selection. - Replace, - /// Removes items from existing selection. - Remove, - /// Adds items to the current selection. - Append, - /// Inverts the selection. - Invert - } + private IReadOnlyCollection _items = Array.Empty(); + private IReadOnlyList _initialSelection = Array.Empty(); + private Rect _selectedArea; /// Attempts to start a new selection. + /// The containers that can be part of the selection. /// The location inside the graph. /// The type of selection. /// Will not do anything if selection is in progress. - public void Start(Point location, SelectionType selectionType) + public Rect Start(IEnumerable containers, Point location, SelectionType selectionType, bool realtime) { - if (!_host.IsSelecting) - { - _selectionType = selectionType; - _initialSelection = _host.SelectedContainers; + _items = containers.Where(x => x.IsSelectable).ToList(); + _initialSelection = containers.Where(x => x.IsSelected).ToList(); - _isRealtime = _host.EnableRealtimeSelection; - _startLocation = location; + _selectionType = selectionType; - _host.SelectedArea = new Rect(); - _host.IsSelecting = true; - } + _isRealtime = realtime; + _startLocation = location; + _endLocation = location; + + _selectedArea = new Rect(); + return _selectedArea; } /// Update the end location for the selection. /// An absolute location. - public void Update(Point endLocation) + public Rect Update(Point 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); + _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); - _host.SelectedArea = new Rect(left, top, width, height); + _selectedArea = new Rect(left, top, width, height); if (_isRealtime) { - PreviewSelection(_host.SelectedArea); + PreviewSelection(_selectedArea); } + + return _selectedArea; } - /// Commits the current selection to the editor. - public void End() + /// Increase the selected area by the specified amount. + public Rect Update(Vector amount) { - if (_host.IsSelecting) - { - PreviewSelection(_host.SelectedArea); + _endLocation += amount; - _host.ApplyPreviewingSelection(); - _host.IsSelecting = false; - } + return Update(_endLocation); } - /// Aborts the current selection. - public void Abort() + /// Commits the current selection to the editor. + public Rect End() { - if (_host.IsSelecting) - { - _host.ClearPreviewingSelection(); - _host.IsSelecting = false; - } + PreviewSelection(_selectedArea); + _items = Array.Empty(); + _initialSelection = Array.Empty(); + + return _selectedArea; } private void PreviewSelection(Rect area) @@ -128,10 +113,8 @@ private void PreviewSelection(Rect area) private void PreviewUnselectAll() { - ItemCollection items = _host.Items; - for (var i = 0; i < items.Count; i++) + foreach (var container in _items) { - var container = (ItemContainer)_host.ItemContainerGenerator.ContainerFromIndex(i); container.IsPreviewingSelection = false; } } @@ -145,10 +128,8 @@ private void PreviewSelectArea(Rect area, bool append = false, bool fit = false) if (area.X != 0 || area.Y != 0 || area.Width > 0 || area.Height > 0) { - ItemCollection items = _host.Items; - for (var i = 0; i < items.Count; i++) + foreach (var container in _items) { - var container = (ItemContainer)_host.ItemContainerGenerator.ContainerFromIndex(i); if (container.IsSelectableInArea(area, fit)) { container.IsPreviewingSelection = true; @@ -159,10 +140,8 @@ private void PreviewSelectArea(Rect area, bool append = false, bool fit = false) private void PreviewUnselectArea(Rect area, bool fit = false) { - ItemCollection items = _host.Items; - for (var i = 0; i < items.Count; i++) + foreach (var container in _items) { - var container = (ItemContainer)_host.ItemContainerGenerator.ContainerFromIndex(i); if (container.IsSelectableInArea(area, fit)) { container.IsPreviewingSelection = false; @@ -180,10 +159,8 @@ private static void PreviewSelectContainers(IReadOnlyList contain private void PreviewInvertSelection(Rect area, bool fit = false) { - ItemCollection items = _host.Items; - for (var i = 0; i < items.Count; i++) + foreach (var container in _items) { - var container = (ItemContainer)_host.ItemContainerGenerator.ContainerFromIndex(i); if (container.IsSelectableInArea(area, fit)) { container.IsPreviewingSelection = !container.IsPreviewingSelection; diff --git a/Nodify/NodifyEditor.Selecting.cs b/Nodify/NodifyEditor.Selecting.cs new file mode 100644 index 00000000..cac8376a --- /dev/null +++ b/Nodify/NodifyEditor.Selecting.cs @@ -0,0 +1,517 @@ +using System.Diagnostics; +using System.Windows.Controls.Primitives; +using System.Windows.Controls; +using System.Windows; +using System.Collections; +using System.Collections.Specialized; +using System.Collections.Generic; +using System.Windows.Input; +using System.Windows.Shapes; + +namespace Nodify +{ + /// Available selection logic. + public enum SelectionType + { + /// Replaces the old selection. + Replace, + /// Removes items from existing selection. + Remove, + /// Adds items to the current selection. + Append, + /// Inverts the selection. + Invert + } + + [StyleTypedProperty(Property = nameof(SelectionRectangleStyle), StyleTargetType = typeof(Rectangle))] + public partial class NodifyEditor : MultiSelector + { + public static readonly DependencyProperty ItemsSelectStartedCommandProperty = DependencyProperty.Register(nameof(ItemsSelectStartedCommand), typeof(ICommand), typeof(NodifyEditor)); + public static readonly DependencyProperty ItemsSelectCompletedCommandProperty = DependencyProperty.Register(nameof(ItemsSelectCompletedCommand), typeof(ICommand), typeof(NodifyEditor)); + + public static readonly DependencyProperty SelectionRectangleStyleProperty = DependencyProperty.Register(nameof(SelectionRectangleStyle), typeof(Style), typeof(NodifyEditor)); + + protected static readonly DependencyPropertyKey SelectedAreaPropertyKey = DependencyProperty.RegisterReadOnly(nameof(SelectedArea), typeof(Rect), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.Rect)); + public static readonly DependencyProperty SelectedAreaProperty = SelectedAreaPropertyKey.DependencyProperty; + + protected static readonly DependencyPropertyKey IsSelectingPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsSelecting), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False, OnIsSelectingChanged)); + public static readonly DependencyProperty IsSelectingProperty = IsSelectingPropertyKey.DependencyProperty; + + public static readonly DependencyProperty EnableRealtimeSelectionProperty = DependencyProperty.Register(nameof(EnableRealtimeSelection), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False)); + public static readonly DependencyProperty CanSelectMultipleConnectionsProperty = DependencyProperty.Register(nameof(CanSelectMultipleConnections), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.True)); + public static readonly DependencyProperty CanSelectMultipleItemsProperty = DependencyProperty.Register(nameof(CanSelectMultipleItems), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.True, OnCanSelectMultipleItemsChanged, CoerceCanSelectMultipleItems)); + public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(nameof(SelectedItems), typeof(IList), typeof(NodifyEditor), new FrameworkPropertyMetadata(default(IList), OnSelectedItemsSourceChanged)); + public static readonly DependencyProperty SelectedConnectionsProperty = DependencyProperty.Register(nameof(SelectedConnections), typeof(IList), typeof(NodifyEditor), new FrameworkPropertyMetadata(default(IList))); + public static readonly DependencyProperty SelectedConnectionProperty = DependencyProperty.Register(nameof(SelectedConnection), typeof(object), typeof(NodifyEditor), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + + private static void OnCanSelectMultipleItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + => ((NodifyEditor)d).CanSelectMultipleItemsBase = (bool)e.NewValue; + + private static object CoerceCanSelectMultipleItems(DependencyObject d, object baseValue) + => ((NodifyEditor)d).CanSelectMultipleItemsBase = (bool)baseValue; + + private static void OnSelectedItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + => ((NodifyEditor)d).OnSelectedItemsSourceChanged((IList)e.OldValue, (IList)e.NewValue); + + private static void OnIsSelectingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var editor = (NodifyEditor)d; + if ((bool)e.NewValue == true) + editor.OnItemsSelectStarted(); + else + editor.OnItemsSelectCompleted(); + } + + private void OnItemsSelectCompleted() + { + if (ItemsSelectCompletedCommand?.CanExecute(DataContext) ?? false) + ItemsSelectCompletedCommand.Execute(DataContext); + } + + private void OnItemsSelectStarted() + { + if (ItemsSelectStartedCommand?.CanExecute(DataContext) ?? false) + ItemsSelectStartedCommand.Execute(DataContext); + } + + /// Invoked when a selection operation is started. + public ICommand? ItemsSelectStartedCommand + { + get => (ICommand?)GetValue(ItemsSelectStartedCommandProperty); + set => SetValue(ItemsSelectStartedCommandProperty, value); + } + + /// Invoked when a selection operation is completed. + public ICommand? ItemsSelectCompletedCommand + { + get => (ICommand?)GetValue(ItemsSelectCompletedCommandProperty); + set => SetValue(ItemsSelectCompletedCommandProperty, value); + } + + /// + /// Gets or sets whether multiple connections can be selected. + /// + public bool CanSelectMultipleConnections + { + get => (bool)GetValue(CanSelectMultipleConnectionsProperty); + set => SetValue(CanSelectMultipleConnectionsProperty, value); + } + + /// + /// Gets or sets whether multiple s can be selected. + /// + public new bool CanSelectMultipleItems + { + get => (bool)GetValue(CanSelectMultipleItemsProperty); + set => SetValue(CanSelectMultipleItemsProperty, value); + } + + private bool CanSelectMultipleItemsBase + { + get => base.CanSelectMultipleItems; + set => base.CanSelectMultipleItems = value; + } + + /// + /// Enables selecting and deselecting items while the changes. + /// Disable for maximum performance when hundreds of items are generated. + /// + public bool EnableRealtimeSelection + { + get => (bool)GetValue(EnableRealtimeSelectionProperty); + set => SetValue(EnableRealtimeSelectionProperty, value); + } + + /// + /// Gets or sets the selected connection. + /// + public object? SelectedConnection + { + get => GetValue(SelectedConnectionProperty); + set => SetValue(SelectedConnectionProperty, value); + } + + /// + /// Gets or sets the selected connections in the . + /// + public IList? SelectedConnections + { + get => (IList?)GetValue(SelectedConnectionsProperty); + set => SetValue(SelectedConnectionsProperty, value); + } + + /// + /// Gets or sets the selected items in the . + /// + public new IList? SelectedItems + { + get => (IList?)GetValue(SelectedItemsProperty); + set => SetValue(SelectedItemsProperty, value); + } + + /// + /// Gets the currently selected area while is true. + /// + public Rect SelectedArea + { + get => (Rect)GetValue(SelectedAreaProperty); + private set => SetValue(SelectedAreaPropertyKey, value); + } + + /// + /// Gets a value that indicates whether a selection operation is in progress. + /// + public bool IsSelecting + { + get => (bool)GetValue(IsSelectingProperty); + private set => SetValue(IsSelectingPropertyKey, value); + } + + /// + /// Gets or sets the style to use for the selection rectangle. + /// + public Style SelectionRectangleStyle + { + get => (Style)GetValue(SelectionRectangleStyleProperty); + set => SetValue(SelectionRectangleStyleProperty, value); + } + + /// + /// Gets a list of s that are selected. + /// + /// Cache the result before using it to avoid extra allocations. + protected internal IReadOnlyList SelectedContainers + { + get + { + IList selectedItems = base.SelectedItems; + var selectedContainers = new List(selectedItems.Count); + + for (var i = 0; i < selectedItems.Count; i++) + { + var container = (ItemContainer)ItemContainerGenerator.ContainerFromItem(selectedItems[i]); + selectedContainers.Add(container); + } + + return selectedContainers; + } + } + + /// + /// Gets or sets whether cancelling a selection operation is allowed (see ). + /// + public static bool AllowSelectionCancellation { get; set; } = true; + + /// The selection helper. + private readonly SelectionHelper _selection = new SelectionHelper(); + + #region Selection + + internal void ApplyPreviewingSelection() + { + Debug.Assert(IsSelecting); + + ItemCollection items = Items; + IList selected = base.SelectedItems; + + BeginUpdateSelectedItems(); + for (var i = 0; i < items.Count; i++) + { + var container = (ItemContainer)ItemContainerGenerator.ContainerFromIndex(i); + if (container.IsPreviewingSelection == true && container.IsSelectable) + { + selected.Add(items[i]); + } + else if (container.IsPreviewingSelection == false) + { + selected.Remove(items[i]); + } + container.IsPreviewingSelection = null; + } + EndUpdateSelectedItems(); + } + + internal void ClearPreviewingSelection() + { + Debug.Assert(IsSelecting); + + ItemCollection items = Items; + for (var i = 0; i < items.Count; i++) + { + var container = (ItemContainer)ItemContainerGenerator.ContainerFromIndex(i); + container.IsPreviewingSelection = null; + } + } + + /// + /// Inverts the s selection in the specified . + /// + /// The area to look for s. + /// True to check if the contains the .
False to check if intersects the . + public void InvertSelection(Rect area, bool fit = false) + { + ItemCollection items = Items; + IList selected = base.SelectedItems; + + IsSelecting = true; + BeginUpdateSelectedItems(); + for (var i = 0; i < items.Count; i++) + { + var container = (ItemContainer)ItemContainerGenerator.ContainerFromIndex(i); + + if (container.IsSelectableInArea(area, fit)) + { + object? item = items[i]; + if (container.IsSelected) + { + selected.Remove(item); + } + else + { + selected.Add(item); + } + } + } + EndUpdateSelectedItems(); + IsSelecting = false; + } + + /// + /// Selects the s in the specified . + /// + /// The area to look for s. + /// If true, it will add to the existing selection. + /// True to check if the contains the .
False to check if intersects the . + public void SelectArea(Rect area, bool append = false, bool fit = false) + { + if (!append) + { + UnselectAll(); + } + + ItemCollection items = Items; + IList selected = base.SelectedItems; + + IsSelecting = true; + BeginUpdateSelectedItems(); + for (var i = 0; i < items.Count; i++) + { + var container = (ItemContainer)ItemContainerGenerator.ContainerFromIndex(i); + if (container.IsSelectableInArea(area, fit)) + { + selected.Add(items[i]); + } + } + EndUpdateSelectedItems(); + IsSelecting = false; + } + + /// + /// Unselect the s in the specified . + /// + /// The area to look for s. + /// True to check if the contains the .
False to check if intersects the . + public void UnselectArea(Rect area, bool fit = false) + { + IList items = base.SelectedItems; + + IsSelecting = true; + BeginUpdateSelectedItems(); + for (var i = 0; i < items.Count; i++) + { + var container = (ItemContainer)ItemContainerGenerator.ContainerFromItem(items[i]); + if (container.IsSelectableInArea(area, fit)) + { + items.Remove(items[i]); + } + } + EndUpdateSelectedItems(); + IsSelecting = false; + } + + /// + /// Unselect all . + /// + public void UnselectAllConnections() + { + if (ConnectionsHost is MultiSelector selector) + { + selector.UnselectAll(); + } + } + + /// + /// Select all . + /// + public void SelectAllConnections() + { + if (ConnectionsHost is MultiSelector selector) + { + selector.SelectAll(); + } + } + /// + /// Initiates a selection operation from the specified location. + /// + /// This method has no effect if a selection operation is already in progress. + /// The starting point for the selection, in graph space coordinates. + /// The type of selection to perform. Defaults to . + public void BeginSelecting(Point location, SelectionType type = SelectionType.Replace) + { + if (IsSelecting) + { + return; + } + + UnselectAllConnections(); + SelectedArea = _selection.Start(ItemContainers, location, type, EnableRealtimeSelection); + IsSelecting = true; + } + + /// + /// Expands or modifies the selection area by the specified amount. + /// + /// Rrepresents the change to apply to the selection area. + public void UpdateSelection(Vector amount) + { + Debug.Assert(IsSelecting); + SelectedArea = _selection.Update(amount); + } + + /// + /// Expands or modifies the selection area to the specified location. + /// + /// The point, in graph space coordinates, to extend or adjust the selection area to. + public void UpdateSelection(Point location) + { + Debug.Assert(IsSelecting); + SelectedArea = _selection.Update(location); + } + + /// + /// Completes the selection operation and applies any pending changes. + /// + /// This method has no effect if there's no selection operation in progress. + public void EndSelecting() + { + if (!IsSelecting) + { + return; + } + + SelectedArea = _selection.End(); + ApplyPreviewingSelection(); + IsSelecting = false; + } + + /// + /// Cancels the current selection operation and reverts any changes made during the selection process. + /// + /// This method has no effect if there's no selection operation in progress. + public void CancelSelecting() + { + if (!IsSelecting) + { + return; + } + + ClearPreviewingSelection(); + IsSelecting = false; + } + + #endregion + + #region Selection Handlers + + private void OnSelectedItemsSourceChanged(IList oldValue, IList newValue) + { + if (oldValue is INotifyCollectionChanged oc) + { + oc.CollectionChanged -= OnSelectedItemsChanged; + } + + if (newValue is INotifyCollectionChanged nc) + { + nc.CollectionChanged += OnSelectedItemsChanged; + } + + IList selectedItems = base.SelectedItems; + + BeginUpdateSelectedItems(); + selectedItems.Clear(); + if (newValue != null) + { + for (var i = 0; i < newValue.Count; i++) + { + selectedItems.Add(newValue[i]); + } + } + EndUpdateSelectedItems(); + } + + private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (!CanSelectMultipleItems) + return; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Reset: + base.SelectedItems.Clear(); + break; + + case NotifyCollectionChangedAction.Add: + IList? newItems = e.NewItems; + if (newItems != null) + { + IList selectedItems = base.SelectedItems; + for (var i = 0; i < newItems.Count; i++) + { + selectedItems.Add(newItems[i]); + } + } + break; + + case NotifyCollectionChangedAction.Remove: + IList? oldItems = e.OldItems; + if (oldItems != null) + { + IList selectedItems = base.SelectedItems; + for (var i = 0; i < oldItems.Count; i++) + { + selectedItems.Remove(oldItems[i]); + } + } + break; + } + } + + /// + protected override void OnSelectionChanged(SelectionChangedEventArgs e) + { + base.OnSelectionChanged(e); + + IList? selected = SelectedItems; + if (selected != null) + { + IList added = e.AddedItems; + for (var i = 0; i < added.Count; i++) + { + // Ensure no duplicates are added + if (!selected.Contains(added[i])) + { + selected.Add(added[i]); + } + } + + IList removed = e.RemovedItems; + for (var i = 0; i < removed.Count; i++) + { + selected.Remove(removed[i]); + } + } + } + + #endregion + } +} diff --git a/Nodify/NodifyEditor.cs b/Nodify/NodifyEditor.cs index 1f1a7b87..6902532b 100644 --- a/Nodify/NodifyEditor.cs +++ b/Nodify/NodifyEditor.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.Specialized; using System.ComponentModel; using System.Windows; using System.Windows.Controls; @@ -9,7 +8,6 @@ using System.Windows.Input; using System.Windows.Markup; using System.Windows.Media; -using System.Windows.Shapes; namespace Nodify { @@ -20,10 +18,9 @@ namespace Nodify [TemplatePart(Name = ElementConnectionsHost, Type = typeof(FrameworkElement))] [StyleTypedProperty(Property = nameof(ItemContainerStyle), StyleTargetType = typeof(ItemContainer))] [StyleTypedProperty(Property = nameof(DecoratorContainerStyle), StyleTargetType = typeof(DecoratorContainer))] - [StyleTypedProperty(Property = nameof(SelectionRectangleStyle), StyleTargetType = typeof(Rectangle))] [ContentProperty(nameof(Decorators))] [DefaultProperty(nameof(Decorators))] - public partial class NodifyEditor : MultiSelector + public partial class NodifyEditor { protected const string ElementItemsHost = "PART_ItemsHost"; protected const string ElementConnectionsHost = "PART_ConnectionsHost"; @@ -250,7 +247,6 @@ private void ApplyRenderingOptimizations() public static readonly DependencyProperty ConnectionTemplateProperty = DependencyProperty.Register(nameof(ConnectionTemplate), typeof(DataTemplate), typeof(NodifyEditor)); public static readonly DependencyProperty DecoratorTemplateProperty = DependencyProperty.Register(nameof(DecoratorTemplate), typeof(DataTemplate), typeof(NodifyEditor)); public static readonly DependencyProperty PendingConnectionTemplateProperty = DependencyProperty.Register(nameof(PendingConnectionTemplate), typeof(DataTemplate), typeof(NodifyEditor)); - public static readonly DependencyProperty SelectionRectangleStyleProperty = DependencyProperty.Register(nameof(SelectionRectangleStyle), typeof(Style), typeof(NodifyEditor)); public static readonly DependencyProperty DecoratorContainerStyleProperty = DependencyProperty.Register(nameof(DecoratorContainerStyle), typeof(Style), typeof(NodifyEditor)); /// @@ -308,15 +304,6 @@ public DataTemplate PendingConnectionTemplate set => SetValue(PendingConnectionTemplateProperty, value); } - /// - /// Gets or sets the style to use for the selection rectangle. - /// - public Style SelectionRectangleStyle - { - get => (Style)GetValue(SelectionRectangleStyleProperty); - set => SetValue(SelectionRectangleStyleProperty, value); - } - /// /// Gets or sets the style to use for the . /// @@ -330,54 +317,9 @@ public Style DecoratorContainerStyle #region Readonly Dependency Properties - protected static readonly DependencyPropertyKey SelectedAreaPropertyKey = DependencyProperty.RegisterReadOnly(nameof(SelectedArea), typeof(Rect), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.Rect)); - public static readonly DependencyProperty SelectedAreaProperty = SelectedAreaPropertyKey.DependencyProperty; - - protected static readonly DependencyPropertyKey IsSelectingPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsSelecting), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False, OnIsSelectingChanged)); - public static readonly DependencyProperty IsSelectingProperty = IsSelectingPropertyKey.DependencyProperty; - protected static readonly DependencyPropertyKey MouseLocationPropertyKey = DependencyProperty.RegisterReadOnly(nameof(MouseLocation), typeof(Point), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.Point)); public static readonly DependencyProperty MouseLocationProperty = MouseLocationPropertyKey.DependencyProperty; - private static void OnIsSelectingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - var editor = (NodifyEditor)d; - if ((bool)e.NewValue == true) - editor.OnItemsSelectStarted(); - else - editor.OnItemsSelectCompleted(); - } - - private void OnItemsSelectCompleted() - { - if (ItemsSelectCompletedCommand?.CanExecute(DataContext) ?? false) - ItemsSelectCompletedCommand.Execute(DataContext); - } - - private void OnItemsSelectStarted() - { - if (ItemsSelectStartedCommand?.CanExecute(DataContext) ?? false) - ItemsSelectStartedCommand.Execute(DataContext); - } - - /// - /// Gets the currently selected area while is true. - /// - public Rect SelectedArea - { - get => (Rect)GetValue(SelectedAreaProperty); - internal set => SetValue(SelectedAreaPropertyKey, value); - } - - /// - /// Gets a value that indicates whether a selection operation is in progress. - /// - public bool IsSelecting - { - get => (bool)GetValue(IsSelectingProperty); - internal set => SetValue(IsSelectingPropertyKey, value); - } - /// /// Gets the current mouse location in graph space coordinates (relative to the ). /// @@ -392,25 +334,10 @@ public Point MouseLocation #region Dependency Properties public static readonly DependencyProperty ConnectionsProperty = DependencyProperty.Register(nameof(Connections), typeof(IEnumerable), typeof(NodifyEditor)); - public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(nameof(SelectedItems), typeof(IList), typeof(NodifyEditor), new FrameworkPropertyMetadata(default(IList), OnSelectedItemsSourceChanged)); - public static readonly DependencyProperty SelectedConnectionsProperty = DependencyProperty.Register(nameof(SelectedConnections), typeof(IList), typeof(NodifyEditor), new FrameworkPropertyMetadata(default(IList))); - public static readonly DependencyProperty SelectedConnectionProperty = DependencyProperty.Register(nameof(SelectedConnection), typeof(object), typeof(NodifyEditor), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public static readonly DependencyProperty PendingConnectionProperty = DependencyProperty.Register(nameof(PendingConnection), typeof(object), typeof(NodifyEditor)); public static readonly DependencyProperty GridCellSizeProperty = DependencyProperty.Register(nameof(GridCellSize), typeof(uint), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.UInt1, OnGridCellSizeChanged, OnCoerceGridCellSize)); public static readonly DependencyProperty DisableZoomingProperty = DependencyProperty.Register(nameof(DisableZooming), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False)); - public static readonly DependencyProperty EnableRealtimeSelectionProperty = DependencyProperty.Register(nameof(EnableRealtimeSelection), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False)); public static readonly DependencyProperty DecoratorsProperty = DependencyProperty.Register(nameof(Decorators), typeof(IEnumerable), typeof(NodifyEditor)); - public static readonly DependencyProperty CanSelectMultipleConnectionsProperty = DependencyProperty.Register(nameof(CanSelectMultipleConnections), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.True)); - public static readonly DependencyProperty CanSelectMultipleItemsProperty = DependencyProperty.Register(nameof(CanSelectMultipleItems), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.True, OnCanSelectMultipleItemsChanged, CoerceCanSelectMultipleItems)); - - private static void OnCanSelectMultipleItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - => ((NodifyEditor)d).CanSelectMultipleItemsBase = (bool)e.NewValue; - - private static object CoerceCanSelectMultipleItems(DependencyObject d, object baseValue) - => ((NodifyEditor)d).CanSelectMultipleItemsBase = (bool)baseValue; - - private static void OnSelectedItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - => ((NodifyEditor)d).OnSelectedItemsSourceChanged((IList)e.OldValue, (IList)e.NewValue); private static object OnCoerceGridCellSize(DependencyObject d, object value) => (uint)value > 0u ? value : BoxValue.UInt1; @@ -453,33 +380,6 @@ public object PendingConnection set => SetValue(PendingConnectionProperty, value); } - /// - /// Gets or sets the selected connection. - /// - public object? SelectedConnection - { - get => GetValue(SelectedConnectionProperty); - set => SetValue(SelectedConnectionProperty, value); - } - - /// - /// Gets or sets the selected connections in the . - /// - public IList? SelectedConnections - { - get => (IList?)GetValue(SelectedConnectionsProperty); - set => SetValue(SelectedConnectionsProperty, value); - } - - /// - /// Gets or sets the selected items in the . - /// - public new IList? SelectedItems - { - get => (IList?)GetValue(SelectedItemsProperty); - set => SetValue(SelectedItemsProperty, value); - } - /// /// Gets or sets whether zooming should be disabled. /// @@ -489,40 +389,6 @@ public bool DisableZooming set => SetValue(DisableZoomingProperty, value); } - /// - /// Enables selecting and deselecting items while the changes. - /// Disable for maximum performance when hundreds of items are generated. - /// - public bool EnableRealtimeSelection - { - get => (bool)GetValue(EnableRealtimeSelectionProperty); - set => SetValue(EnableRealtimeSelectionProperty, value); - } - - /// - /// Gets or sets whether multiple connections can be selected. - /// - public bool CanSelectMultipleConnections - { - get => (bool)GetValue(CanSelectMultipleConnectionsProperty); - set => SetValue(CanSelectMultipleConnectionsProperty, value); - } - - /// - /// Gets or sets whether multiple s can be selected. - /// - public new bool CanSelectMultipleItems - { - get => (bool)GetValue(CanSelectMultipleItemsProperty); - set => SetValue(CanSelectMultipleItemsProperty, value); - } - - private bool CanSelectMultipleItemsBase - { - get => base.CanSelectMultipleItems; - set => base.CanSelectMultipleItems = value; - } - #endregion #region Command Dependency Properties @@ -533,8 +399,6 @@ private bool CanSelectMultipleItemsBase public static readonly DependencyProperty RemoveConnectionCommandProperty = DependencyProperty.Register(nameof(RemoveConnectionCommand), typeof(ICommand), typeof(NodifyEditor)); public static readonly DependencyProperty ItemsDragStartedCommandProperty = DependencyProperty.Register(nameof(ItemsDragStartedCommand), typeof(ICommand), typeof(NodifyEditor)); public static readonly DependencyProperty ItemsDragCompletedCommandProperty = DependencyProperty.Register(nameof(ItemsDragCompletedCommand), typeof(ICommand), typeof(NodifyEditor)); - public static readonly DependencyProperty ItemsSelectStartedCommandProperty = DependencyProperty.Register(nameof(ItemsSelectStartedCommand), typeof(ICommand), typeof(NodifyEditor)); - public static readonly DependencyProperty ItemsSelectCompletedCommandProperty = DependencyProperty.Register(nameof(ItemsSelectCompletedCommand), typeof(ICommand), typeof(NodifyEditor)); /// /// Invoked when the is completed.
@@ -581,7 +445,7 @@ public ICommand? RemoveConnectionCommand } /// - /// Invoked when a drag operation starts for the . + /// Invoked when a drag operation starts for the , or when is set to true. /// public ICommand? ItemsDragStartedCommand { @@ -590,7 +454,7 @@ public ICommand? ItemsDragStartedCommand } /// - /// Invoked when a drag operation is completed for the . + /// Invoked when a drag operation is completed for the , or when is set to false. /// public ICommand? ItemsDragCompletedCommand { @@ -598,20 +462,6 @@ public ICommand? ItemsDragCompletedCommand set => SetValue(ItemsDragCompletedCommandProperty, value); } - /// Invoked when a selection operation is started. - public ICommand? ItemsSelectStartedCommand - { - get => (ICommand?)GetValue(ItemsSelectStartedCommandProperty); - set => SetValue(ItemsSelectStartedCommandProperty, value); - } - - /// Invoked when a selection operation is completed. - public ICommand? ItemsSelectCompletedCommand - { - get => (ICommand?)GetValue(ItemsSelectCompletedCommandProperty); - set => SetValue(ItemsSelectCompletedCommandProperty, value); - } - #endregion #region Fields @@ -664,27 +514,6 @@ public ICommand? ItemsSelectCompletedCommand private IDraggingStrategy? _draggingStrategy; - /// - /// Gets a list of s that are selected. - /// - /// Cache the result before using it to avoid extra allocations. - protected internal IReadOnlyList SelectedContainers - { - get - { - IList selectedItems = base.SelectedItems; - var selectedContainers = new List(selectedItems.Count); - - for (var i = 0; i < selectedItems.Count; i++) - { - var container = (ItemContainer)ItemContainerGenerator.ContainerFromItem(selectedItems[i]); - selectedContainers.Add(container); - } - - return selectedContainers; - } - } - /// /// Gets a list of all s. /// @@ -1041,245 +870,6 @@ protected override void OnKeyDown(KeyEventArgs e) #endregion - #region Selection Handlers - - private void OnSelectedItemsSourceChanged(IList oldValue, IList newValue) - { - if (oldValue is INotifyCollectionChanged oc) - { - oc.CollectionChanged -= OnSelectedItemsChanged; - } - - if (newValue is INotifyCollectionChanged nc) - { - nc.CollectionChanged += OnSelectedItemsChanged; - } - - IList selectedItems = base.SelectedItems; - - BeginUpdateSelectedItems(); - selectedItems.Clear(); - if (newValue != null) - { - for (var i = 0; i < newValue.Count; i++) - { - selectedItems.Add(newValue[i]); - } - } - EndUpdateSelectedItems(); - } - - private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - if (!CanSelectMultipleItems) - return; - - switch (e.Action) - { - case NotifyCollectionChangedAction.Reset: - base.SelectedItems.Clear(); - break; - - case NotifyCollectionChangedAction.Add: - IList? newItems = e.NewItems; - if (newItems != null) - { - IList selectedItems = base.SelectedItems; - for (var i = 0; i < newItems.Count; i++) - { - selectedItems.Add(newItems[i]); - } - } - break; - - case NotifyCollectionChangedAction.Remove: - IList? oldItems = e.OldItems; - if (oldItems != null) - { - IList selectedItems = base.SelectedItems; - for (var i = 0; i < oldItems.Count; i++) - { - selectedItems.Remove(oldItems[i]); - } - } - break; - } - } - - /// - protected override void OnSelectionChanged(SelectionChangedEventArgs e) - { - base.OnSelectionChanged(e); - - IList? selected = SelectedItems; - if (selected != null) - { - IList added = e.AddedItems; - for (var i = 0; i < added.Count; i++) - { - // Ensure no duplicates are added - if (!selected.Contains(added[i])) - { - selected.Add(added[i]); - } - } - - IList removed = e.RemovedItems; - for (var i = 0; i < removed.Count; i++) - { - selected.Remove(removed[i]); - } - } - } - - #endregion - - #region Selection - - internal void ApplyPreviewingSelection() - { - ItemCollection items = Items; - IList selected = base.SelectedItems; - - IsSelecting = true; - BeginUpdateSelectedItems(); - for (var i = 0; i < items.Count; i++) - { - var container = (ItemContainer)ItemContainerGenerator.ContainerFromIndex(i); - if (container.IsPreviewingSelection == true && container.IsSelectable) - { - selected.Add(items[i]); - } - else if (container.IsPreviewingSelection == false) - { - selected.Remove(items[i]); - } - container.IsPreviewingSelection = null; - } - EndUpdateSelectedItems(); - IsSelecting = false; - } - - internal void ClearPreviewingSelection() - { - ItemCollection items = Items; - for (var i = 0; i < items.Count; i++) - { - var container = (ItemContainer)ItemContainerGenerator.ContainerFromIndex(i); - container.IsPreviewingSelection = null; - } - } - - /// - /// Inverts the s selection in the specified . - /// - /// The area to look for s. - /// True to check if the contains the .
False to check if intersects the . - public void InvertSelection(Rect area, bool fit = false) - { - ItemCollection items = Items; - IList selected = base.SelectedItems; - - IsSelecting = true; - BeginUpdateSelectedItems(); - for (var i = 0; i < items.Count; i++) - { - var container = (ItemContainer)ItemContainerGenerator.ContainerFromIndex(i); - - if (container.IsSelectableInArea(area, fit)) - { - object? item = items[i]; - if (container.IsSelected) - { - selected.Remove(item); - } - else - { - selected.Add(item); - } - } - } - EndUpdateSelectedItems(); - IsSelecting = false; - } - - /// - /// Selects the s in the specified . - /// - /// The area to look for s. - /// If true, it will add to the existing selection. - /// True to check if the contains the .
False to check if intersects the . - public void SelectArea(Rect area, bool append = false, bool fit = false) - { - if (!append) - { - UnselectAll(); - } - - ItemCollection items = Items; - IList selected = base.SelectedItems; - - IsSelecting = true; - BeginUpdateSelectedItems(); - for (var i = 0; i < items.Count; i++) - { - var container = (ItemContainer)ItemContainerGenerator.ContainerFromIndex(i); - if (container.IsSelectableInArea(area, fit)) - { - selected.Add(items[i]); - } - } - EndUpdateSelectedItems(); - IsSelecting = false; - } - - /// - /// Unselect the s in the specified . - /// - /// The area to look for s. - /// True to check if the contains the .
False to check if intersects the . - public void UnselectArea(Rect area, bool fit = false) - { - IList items = base.SelectedItems; - - IsSelecting = true; - BeginUpdateSelectedItems(); - for (var i = 0; i < items.Count; i++) - { - var container = (ItemContainer)ItemContainerGenerator.ContainerFromItem(items[i]); - if (container.IsSelectableInArea(area, fit)) - { - items.Remove(items[i]); - } - } - EndUpdateSelectedItems(); - IsSelecting = false; - } - - /// - /// Unselect all . - /// - public void UnselectAllConnection() - { - if (ConnectionsHost is MultiSelector selector) - { - selector.UnselectAll(); - } - } - - /// - /// Select all . - /// - public void SelectAllConnections() - { - if (ConnectionsHost is MultiSelector selector) - { - selector.SelectAll(); - } - } - - #endregion - #region Dragging private void OnItemsDragDelta(object sender, DragDeltaEventArgs e)