diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c11a5ab..5b1f19a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,13 @@ #### **In development** > - Breaking Changes: +> - Made the setter of NodifyEditor.IsPanning private > - Features: +> - Added BeginPanning, UpdatePanning, EndPanning, CancelPanning and AllowPanningCancellation to NodifyEditor > - Bugfixes: #### **Version 6.6.0** -> - Breaking Changes: > - Features: > - Added InputGroupStyle and OutputGroupStyle to Node > - Added PanWithMouseWheel, PanHorizontalModifierKey and PanVerticalModifierKey to EditorGestures.Editor diff --git a/Nodify/EditorStates/EditorPanningState.cs b/Nodify/EditorStates/EditorPanningState.cs index 74b7be5b..d85aef9d 100644 --- a/Nodify/EditorStates/EditorPanningState.cs +++ b/Nodify/EditorStates/EditorPanningState.cs @@ -10,6 +10,8 @@ public class EditorPanningState : EditorState private Point _previousMousePosition; private Point _currentMousePosition; + private bool Canceled { get; set; } = NodifyEditor.AllowPanningCancellation; + /// Constructs an instance of the state. /// The owner of the state. public EditorPanningState(NodifyEditor editor) : base(editor) @@ -18,22 +20,34 @@ public EditorPanningState(NodifyEditor editor) : base(editor) /// public override void Exit() - => Editor.IsPanning = false; + { + if (Canceled) + { + Editor.CancelPanning(); + } + else + { + Editor.EndPanning(); + } + } /// public override void Enter(EditorState? from) { + Canceled = false; + _initialMousePosition = Mouse.GetPosition(Editor); _previousMousePosition = _initialMousePosition; _currentMousePosition = _initialMousePosition; - Editor.IsPanning = true; + + Editor.BeginPanning(); } /// public override void HandleMouseMove(MouseEventArgs e) { _currentMousePosition = e.GetPosition(Editor); - Editor.ViewportLocation -= (_currentMousePosition - _previousMousePosition) / Editor.ViewportZoom; + Editor.UpdatePanning((_currentMousePosition - _previousMousePosition) / Editor.ViewportZoom); _previousMousePosition = _currentMousePosition; } @@ -65,6 +79,23 @@ public override void HandleMouseUp(MouseButtonEventArgs e) PushState(new EditorPanningState(Editor)); } } + else if (NodifyEditor.AllowPanningCancellation && gestures.CancelAction.Matches(e.Source, e)) + { + Canceled = true; + e.Handled = true; // prevents opening context menu + + PopState(); + } + } + + public override void HandleKeyUp(KeyEventArgs e) + { + EditorGestures.NodifyEditorGestures gestures = EditorGestures.Mappings.Editor; + if (NodifyEditor.AllowPanningCancellation && gestures.CancelAction.Matches(e.Source, e)) + { + Canceled = true; + PopState(); + } } } } diff --git a/Nodify/NodifyEditor.Panning.cs b/Nodify/NodifyEditor.Panning.cs new file mode 100644 index 00000000..bae965ad --- /dev/null +++ b/Nodify/NodifyEditor.Panning.cs @@ -0,0 +1,215 @@ +using System; +using System.Diagnostics; +using System.Windows; +using System.Windows.Input; +using System.Windows.Threading; + +namespace Nodify +{ + public partial class NodifyEditor + { + public static readonly DependencyProperty AutoPanSpeedProperty = DependencyProperty.Register(nameof(AutoPanSpeed), typeof(double), typeof(NodifyEditor), new FrameworkPropertyMetadata(15d)); + public static readonly DependencyProperty AutoPanEdgeDistanceProperty = DependencyProperty.Register(nameof(AutoPanEdgeDistance), typeof(double), typeof(NodifyEditor), new FrameworkPropertyMetadata(15d)); + public static readonly DependencyProperty DisableAutoPanningProperty = DependencyProperty.Register(nameof(DisableAutoPanning), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False, OnDisableAutoPanningChanged)); + public static readonly DependencyProperty DisablePanningProperty = DependencyProperty.Register(nameof(DisablePanning), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False, OnDisablePanningChanged)); + + protected static readonly DependencyPropertyKey IsPanningPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsPanning), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False)); + public static readonly DependencyProperty IsPanningProperty = IsPanningPropertyKey.DependencyProperty; + + private static void OnDisableAutoPanningChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + => ((NodifyEditor)d).OnDisableAutoPanningChanged((bool)e.NewValue); + + private static void OnDisablePanningChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var editor = (NodifyEditor)d; + editor.OnDisableAutoPanningChanged(editor.DisableAutoPanning || editor.DisablePanning); + } + + /// + /// Gets or sets whether panning should be disabled. + /// + public bool DisablePanning + { + get => (bool)GetValue(DisablePanningProperty); + set => SetValue(DisablePanningProperty, value); + } + + /// + /// Gets or sets whether to disable the auto panning when selecting or dragging near the edge of the editor configured by . + /// + public bool DisableAutoPanning + { + get => (bool)GetValue(DisableAutoPanningProperty); + set => SetValue(DisableAutoPanningProperty, value); + } + + /// + /// Gets or sets the speed used when auto-panning scaled by + /// + public double AutoPanSpeed + { + get => (double)GetValue(AutoPanSpeedProperty); + set => SetValue(AutoPanSpeedProperty, value); + } + + /// + /// Gets or sets the maximum distance in pixels from the edge of the editor that will trigger auto-panning. + /// + public double AutoPanEdgeDistance + { + get => (double)GetValue(AutoPanEdgeDistanceProperty); + set => SetValue(AutoPanEdgeDistanceProperty, value); + } + + /// + /// Gets a value that indicates whether a panning operation is in progress. + /// + public bool IsPanning + { + get => (bool)GetValue(IsPanningProperty); + private set => SetValue(IsPanningPropertyKey, value); + } + + /// + /// Gets or sets whether panning cancellation is allowed (see ). + /// + public static bool AllowPanningCancellation { get; set; } + + /// + /// Gets or sets the maximum number of pixels allowed to move the mouse before cancelling the mouse event. + /// Useful for s to appear if mouse only moved a bit or not at all. + /// + public static double HandleRightClickAfterPanningThreshold { get; set; } = 12d; + + /// + /// Gets or sets how often the new is calculated in milliseconds when is false. + /// + public static double AutoPanningTickRate { get; set; } = 1; + + private DispatcherTimer? _autoPanningTimer; + + private Point _initialPanningLocation; + + /// + /// Starts the panning operation from the specified location. Call to end the panning operation. + /// + /// This method has no effect if a panning operation is already in progress. + /// The initial location where panning starts, in graph space coordinates. + public void BeginPanning(Point location) + { + if (IsPanning) + { + return; + } + + _initialPanningLocation = location; + ViewportLocation = location; + IsPanning = true; + } + + /// + /// Starts the panning operation from the current . + /// + public void BeginPanning() + => BeginPanning(ViewportLocation); + + /// + /// Pans the viewport by the specified amount. + /// + /// The amount to pan the viewport. + /// + /// This method adjusts the current incrementally based on the provided amount. + /// It should only be called while a panning operation is active (see ). + /// + public void UpdatePanning(Vector amount) + { + Debug.Assert(IsPanning); + ViewportLocation -= amount; + } + + /// + /// Cancels the current panning operation and reverts the viewport to its initial location if is true. + /// + /// This method has no effect if there's no panning operation in progress. + public void CancelPanning() + { + if (!AllowPanningCancellation || !IsPanning) + { + return; + } + + ViewportLocation = _initialPanningLocation; + IsPanning = false; + } + + /// + /// Ends the current panning operation, retaining the current . + /// + /// This method has no effect if there's no panning operation in progress. + public void EndPanning() + { + IsPanning = false; + } + + #region Auto panning + + private void HandleAutoPanning(object? sender, EventArgs e) + { + if (!IsPanning && IsMouseCaptureWithin) + { + Point mousePosition = Mouse.GetPosition(this); + double edgeDistance = AutoPanEdgeDistance; + double autoPanSpeed = Math.Min(AutoPanSpeed, AutoPanSpeed * AutoPanningTickRate) / (ViewportZoom * 2); + double x = ViewportLocation.X; + double y = ViewportLocation.Y; + + if (mousePosition.X <= edgeDistance) + { + x -= autoPanSpeed; + } + else if (mousePosition.X >= ActualWidth - edgeDistance) + { + x += autoPanSpeed; + } + + if (mousePosition.Y <= edgeDistance) + { + y -= autoPanSpeed; + } + else if (mousePosition.Y >= ActualHeight - edgeDistance) + { + y += autoPanSpeed; + } + + ViewportLocation = new Point(x, y); + MouseLocation = Mouse.GetPosition(ItemsHost); + + State.HandleAutoPanning(new MouseEventArgs(Mouse.PrimaryDevice, 0)); + } + } + + /// + /// Called when the changes. + /// + /// Whether to enable or disable auto panning. + private void OnDisableAutoPanningChanged(bool shouldDisable) + { + if (shouldDisable) + { + _autoPanningTimer?.Stop(); + } + else if (_autoPanningTimer == null) + { + _autoPanningTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(AutoPanningTickRate), + DispatcherPriority.Background, HandleAutoPanning, Dispatcher); + } + else + { + _autoPanningTimer.Interval = TimeSpan.FromMilliseconds(AutoPanningTickRate); + _autoPanningTimer.Start(); + } + } + + #endregion + } +} diff --git a/Nodify/NodifyEditor.cs b/Nodify/NodifyEditor.cs index 97d936b4..d90182a9 100644 --- a/Nodify/NodifyEditor.cs +++ b/Nodify/NodifyEditor.cs @@ -10,7 +10,6 @@ using System.Windows.Markup; using System.Windows.Media; using System.Windows.Shapes; -using System.Windows.Threading; namespace Nodify { @@ -249,9 +248,6 @@ private void ApplyRenderingOptimizations() public static readonly DependencyProperty BringIntoViewSpeedProperty = DependencyProperty.Register(nameof(BringIntoViewSpeed), typeof(double), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.Double1000)); public static readonly DependencyProperty BringIntoViewMaxDurationProperty = DependencyProperty.Register(nameof(BringIntoViewMaxDuration), typeof(double), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.Double1)); public static readonly DependencyProperty DisplayConnectionsOnTopProperty = DependencyProperty.Register(nameof(DisplayConnectionsOnTop), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False)); - public static readonly DependencyProperty DisableAutoPanningProperty = DependencyProperty.Register(nameof(DisableAutoPanning), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False, OnDisableAutoPanningChanged)); - public static readonly DependencyProperty AutoPanSpeedProperty = DependencyProperty.Register(nameof(AutoPanSpeed), typeof(double), typeof(NodifyEditor), new FrameworkPropertyMetadata(15d)); - public static readonly DependencyProperty AutoPanEdgeDistanceProperty = DependencyProperty.Register(nameof(AutoPanEdgeDistance), typeof(double), typeof(NodifyEditor), new FrameworkPropertyMetadata(15d)); 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)); @@ -259,9 +255,6 @@ private void ApplyRenderingOptimizations() public static readonly DependencyProperty CuttingLineStyleProperty = DependencyProperty.Register(nameof(CuttingLineStyle), typeof(Style), typeof(NodifyEditor)); public static readonly DependencyProperty DecoratorContainerStyleProperty = DependencyProperty.Register(nameof(DecoratorContainerStyle), typeof(Style), typeof(NodifyEditor)); - private static void OnDisableAutoPanningChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - => ((NodifyEditor)d).OnDisableAutoPanningChanged((bool)e.NewValue); - /// /// Gets or sets the maximum animation duration in seconds for bringing a location into view. /// @@ -290,33 +283,6 @@ public bool DisplayConnectionsOnTop set => SetValue(DisplayConnectionsOnTopProperty, value); } - /// - /// Gets or sets whether to disable the auto panning when selecting or dragging near the edge of the editor configured by . - /// - public bool DisableAutoPanning - { - get => (bool)GetValue(DisableAutoPanningProperty); - set => SetValue(DisableAutoPanningProperty, value); - } - - /// - /// Gets or sets the speed used when auto-panning scaled by - /// - public double AutoPanSpeed - { - get => (double)GetValue(AutoPanSpeedProperty); - set => SetValue(AutoPanSpeedProperty, value); - } - - /// - /// Gets or sets the maximum distance in pixels from the edge of the editor that will trigger auto-panning. - /// - public double AutoPanEdgeDistance - { - get => (double)GetValue(AutoPanEdgeDistanceProperty); - set => SetValue(AutoPanEdgeDistanceProperty, value); - } - /// /// Gets or sets the to use when generating a new . /// @@ -390,9 +356,6 @@ public Style DecoratorContainerStyle protected static readonly DependencyPropertyKey IsCuttingPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsCutting), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False, OnIsCuttingChanged)); public static readonly DependencyProperty IsCuttingProperty = IsCuttingPropertyKey.DependencyProperty; - public static readonly DependencyPropertyKey IsPanningPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsPanning), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False)); - public static readonly DependencyProperty IsPanningProperty = IsPanningPropertyKey.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; @@ -483,15 +446,6 @@ public bool IsCutting private set => SetValue(IsCuttingPropertyKey, value); } - /// - /// Gets a value that indicates whether a panning operation is in progress. - /// - public bool IsPanning - { - get => (bool)GetValue(IsPanningProperty); - protected internal set => SetValue(IsPanningPropertyKey, value); - } - /// /// Gets the current mouse location in graph space coordinates (relative to the ). /// @@ -512,7 +466,6 @@ public Point MouseLocation 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 DisablePanningProperty = DependencyProperty.Register(nameof(DisablePanning), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False, OnDisablePanningChanged)); 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)); @@ -532,12 +485,6 @@ private static object OnCoerceGridCellSize(DependencyObject d, object value) private static void OnGridCellSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { } - private static void OnDisablePanningChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - var editor = (NodifyEditor)d; - editor.OnDisableAutoPanningChanged(editor.DisableAutoPanning || editor.DisablePanning); - } - /// /// Gets or sets the items that will be rendered in the decorators layer via s. /// @@ -610,15 +557,6 @@ public bool DisableZooming set => SetValue(DisableZoomingProperty, value); } - /// - /// Gets or sets whether panning should be disabled. - /// - public bool DisablePanning - { - get => (bool)GetValue(DisablePanningProperty); - set => SetValue(DisablePanningProperty, value); - } - /// /// Enables selecting and deselecting items while the changes. /// Disable for maximum performance when hundreds of items are generated. @@ -762,22 +700,11 @@ public ICommand? CuttingCompletedCommand #region Fields - /// - /// Gets or sets the maximum number of pixels allowed to move the mouse before cancelling the mouse event. - /// Useful for s to appear if mouse only moved a bit or not at all. - /// - public static double HandleRightClickAfterPanningThreshold { get; set; } = 12d; - /// /// Correct 's position after moving if starting position is not snapped to grid. /// public static bool EnableSnappingCorrection { get; set; } = true; - /// - /// Gets or sets how often the new is calculated in milliseconds when is false. - /// - public static double AutoPanningTickRate { get; set; } = 1; - /// /// Gets or sets if s should enable optimizations based on and . /// @@ -833,7 +760,6 @@ public ICommand? CuttingCompletedCommand protected internal UIElement ConnectionsHost { get; private set; } = default!; private IDraggingStrategy? _draggingStrategy; - private DispatcherTimer? _autoPanningTimer; /// /// Gets a list of s that are selected. @@ -989,7 +915,7 @@ public void BringIntoView(Point point, bool animated = true, Action? onFinish = if (animated && newLocation != ViewportLocation) { - IsPanning = true; + BeginPanning(); DisablePanning = true; DisableZooming = true; @@ -999,7 +925,7 @@ public void BringIntoView(Point point, bool animated = true, Action? onFinish = this.StartAnimation(ViewportLocationProperty, newLocation, duration, (s, e) => { - IsPanning = false; + EndPanning(); DisablePanning = false; DisableZooming = false; @@ -1044,67 +970,6 @@ public void FitToScreen(Rect? area = null) #endregion - #region Auto panning - - private void HandleAutoPanning(object? sender, EventArgs e) - { - if (!IsPanning && IsMouseCaptureWithin) - { - Point mousePosition = Mouse.GetPosition(this); - double edgeDistance = AutoPanEdgeDistance; - double autoPanSpeed = Math.Min(AutoPanSpeed, AutoPanSpeed * AutoPanningTickRate) / (ViewportZoom * 2); - double x = ViewportLocation.X; - double y = ViewportLocation.Y; - - if (mousePosition.X <= edgeDistance) - { - x -= autoPanSpeed; - } - else if (mousePosition.X >= ActualWidth - edgeDistance) - { - x += autoPanSpeed; - } - - if (mousePosition.Y <= edgeDistance) - { - y -= autoPanSpeed; - } - else if (mousePosition.Y >= ActualHeight - edgeDistance) - { - y += autoPanSpeed; - } - - ViewportLocation = new Point(x, y); - MouseLocation = Mouse.GetPosition(ItemsHost); - - State.HandleAutoPanning(new MouseEventArgs(Mouse.PrimaryDevice, 0)); - } - } - - /// - /// Called when the changes. - /// - /// Whether to enable or disable auto panning. - protected virtual void OnDisableAutoPanningChanged(bool shouldDisable) - { - if (shouldDisable) - { - _autoPanningTimer?.Stop(); - } - else if (_autoPanningTimer == null) - { - _autoPanningTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(AutoPanningTickRate), - DispatcherPriority.Background, HandleAutoPanning, Dispatcher); - } - else - { - _autoPanningTimer.Interval = TimeSpan.FromMilliseconds(AutoPanningTickRate); - _autoPanningTimer.Start(); - } - } - - #endregion - #region Connector handling private void OnConnectorDisconnected(object sender, ConnectorEventArgs e)