Skip to content

Commit 4521531

Browse files
authored
Add panning methods to NodifyEditor (#153)
1 parent 08b6739 commit 4521531

File tree

4 files changed

+253
-141
lines changed

4 files changed

+253
-141
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
#### **In development**
44

55
> - Breaking Changes:
6+
> - Made the setter of NodifyEditor.IsPanning private
67
> - Features:
8+
> - Added BeginPanning, UpdatePanning, EndPanning, CancelPanning and AllowPanningCancellation to NodifyEditor
79
> - Bugfixes:
810
911
#### **Version 6.6.0**
1012

11-
> - Breaking Changes:
1213
> - Features:
1314
> - Added InputGroupStyle and OutputGroupStyle to Node
1415
> - Added PanWithMouseWheel, PanHorizontalModifierKey and PanVerticalModifierKey to EditorGestures.Editor

Nodify/EditorStates/EditorPanningState.cs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ public class EditorPanningState : EditorState
1010
private Point _previousMousePosition;
1111
private Point _currentMousePosition;
1212

13+
private bool Canceled { get; set; } = NodifyEditor.AllowPanningCancellation;
14+
1315
/// <summary>Constructs an instance of the <see cref="EditorPanningState"/> state.</summary>
1416
/// <param name="editor">The owner of the state.</param>
1517
public EditorPanningState(NodifyEditor editor) : base(editor)
@@ -18,22 +20,34 @@ public EditorPanningState(NodifyEditor editor) : base(editor)
1820

1921
/// <inheritdoc />
2022
public override void Exit()
21-
=> Editor.IsPanning = false;
23+
{
24+
if (Canceled)
25+
{
26+
Editor.CancelPanning();
27+
}
28+
else
29+
{
30+
Editor.EndPanning();
31+
}
32+
}
2233

2334
/// <inheritdoc />
2435
public override void Enter(EditorState? from)
2536
{
37+
Canceled = false;
38+
2639
_initialMousePosition = Mouse.GetPosition(Editor);
2740
_previousMousePosition = _initialMousePosition;
2841
_currentMousePosition = _initialMousePosition;
29-
Editor.IsPanning = true;
42+
43+
Editor.BeginPanning();
3044
}
3145

3246
/// <inheritdoc />
3347
public override void HandleMouseMove(MouseEventArgs e)
3448
{
3549
_currentMousePosition = e.GetPosition(Editor);
36-
Editor.ViewportLocation -= (_currentMousePosition - _previousMousePosition) / Editor.ViewportZoom;
50+
Editor.UpdatePanning((_currentMousePosition - _previousMousePosition) / Editor.ViewportZoom);
3751
_previousMousePosition = _currentMousePosition;
3852
}
3953

@@ -65,6 +79,23 @@ public override void HandleMouseUp(MouseButtonEventArgs e)
6579
PushState(new EditorPanningState(Editor));
6680
}
6781
}
82+
else if (NodifyEditor.AllowPanningCancellation && gestures.CancelAction.Matches(e.Source, e))
83+
{
84+
Canceled = true;
85+
e.Handled = true; // prevents opening context menu
86+
87+
PopState();
88+
}
89+
}
90+
91+
public override void HandleKeyUp(KeyEventArgs e)
92+
{
93+
EditorGestures.NodifyEditorGestures gestures = EditorGestures.Mappings.Editor;
94+
if (NodifyEditor.AllowPanningCancellation && gestures.CancelAction.Matches(e.Source, e))
95+
{
96+
Canceled = true;
97+
PopState();
98+
}
6899
}
69100
}
70101
}

Nodify/NodifyEditor.Panning.cs

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Windows;
4+
using System.Windows.Input;
5+
using System.Windows.Threading;
6+
7+
namespace Nodify
8+
{
9+
public partial class NodifyEditor
10+
{
11+
public static readonly DependencyProperty AutoPanSpeedProperty = DependencyProperty.Register(nameof(AutoPanSpeed), typeof(double), typeof(NodifyEditor), new FrameworkPropertyMetadata(15d));
12+
public static readonly DependencyProperty AutoPanEdgeDistanceProperty = DependencyProperty.Register(nameof(AutoPanEdgeDistance), typeof(double), typeof(NodifyEditor), new FrameworkPropertyMetadata(15d));
13+
public static readonly DependencyProperty DisableAutoPanningProperty = DependencyProperty.Register(nameof(DisableAutoPanning), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False, OnDisableAutoPanningChanged));
14+
public static readonly DependencyProperty DisablePanningProperty = DependencyProperty.Register(nameof(DisablePanning), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False, OnDisablePanningChanged));
15+
16+
protected static readonly DependencyPropertyKey IsPanningPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsPanning), typeof(bool), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.False));
17+
public static readonly DependencyProperty IsPanningProperty = IsPanningPropertyKey.DependencyProperty;
18+
19+
private static void OnDisableAutoPanningChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
20+
=> ((NodifyEditor)d).OnDisableAutoPanningChanged((bool)e.NewValue);
21+
22+
private static void OnDisablePanningChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
23+
{
24+
var editor = (NodifyEditor)d;
25+
editor.OnDisableAutoPanningChanged(editor.DisableAutoPanning || editor.DisablePanning);
26+
}
27+
28+
/// <summary>
29+
/// Gets or sets whether panning should be disabled.
30+
/// </summary>
31+
public bool DisablePanning
32+
{
33+
get => (bool)GetValue(DisablePanningProperty);
34+
set => SetValue(DisablePanningProperty, value);
35+
}
36+
37+
/// <summary>
38+
/// Gets or sets whether to disable the auto panning when selecting or dragging near the edge of the editor configured by <see cref="AutoPanEdgeDistance"/>.
39+
/// </summary>
40+
public bool DisableAutoPanning
41+
{
42+
get => (bool)GetValue(DisableAutoPanningProperty);
43+
set => SetValue(DisableAutoPanningProperty, value);
44+
}
45+
46+
/// <summary>
47+
/// Gets or sets the speed used when auto-panning scaled by <see cref="AutoPanningTickRate"/>
48+
/// </summary>
49+
public double AutoPanSpeed
50+
{
51+
get => (double)GetValue(AutoPanSpeedProperty);
52+
set => SetValue(AutoPanSpeedProperty, value);
53+
}
54+
55+
/// <summary>
56+
/// Gets or sets the maximum distance in pixels from the edge of the editor that will trigger auto-panning.
57+
/// </summary>
58+
public double AutoPanEdgeDistance
59+
{
60+
get => (double)GetValue(AutoPanEdgeDistanceProperty);
61+
set => SetValue(AutoPanEdgeDistanceProperty, value);
62+
}
63+
64+
/// <summary>
65+
/// Gets a value that indicates whether a panning operation is in progress.
66+
/// </summary>
67+
public bool IsPanning
68+
{
69+
get => (bool)GetValue(IsPanningProperty);
70+
private set => SetValue(IsPanningPropertyKey, value);
71+
}
72+
73+
/// <summary>
74+
/// Gets or sets whether panning cancellation is allowed (see <see cref="EditorGestures.NodifyEditorGestures.CancelAction"/>).
75+
/// </summary>
76+
public static bool AllowPanningCancellation { get; set; }
77+
78+
/// <summary>
79+
/// Gets or sets the maximum number of pixels allowed to move the mouse before cancelling the mouse event.
80+
/// Useful for <see cref="ContextMenu"/>s to appear if mouse only moved a bit or not at all.
81+
/// </summary>
82+
public static double HandleRightClickAfterPanningThreshold { get; set; } = 12d;
83+
84+
/// <summary>
85+
/// Gets or sets how often the new <see cref="ViewportLocation"/> is calculated in milliseconds when <see cref="DisableAutoPanning"/> is false.
86+
/// </summary>
87+
public static double AutoPanningTickRate { get; set; } = 1;
88+
89+
private DispatcherTimer? _autoPanningTimer;
90+
91+
private Point _initialPanningLocation;
92+
93+
/// <summary>
94+
/// Starts the panning operation from the specified location. Call <see cref="EndPanning"/> to end the panning operation.
95+
/// </summary>
96+
/// <remarks>This method has no effect if a panning operation is already in progress.</remarks>
97+
/// <param name="location">The initial location where panning starts, in graph space coordinates.</param>
98+
public void BeginPanning(Point location)
99+
{
100+
if (IsPanning)
101+
{
102+
return;
103+
}
104+
105+
_initialPanningLocation = location;
106+
ViewportLocation = location;
107+
IsPanning = true;
108+
}
109+
110+
/// <summary>
111+
/// Starts the panning operation from the current <see cref="ViewportLocation" />.
112+
/// </summary>
113+
public void BeginPanning()
114+
=> BeginPanning(ViewportLocation);
115+
116+
/// <summary>
117+
/// Pans the viewport by the specified amount.
118+
/// </summary>
119+
/// <param name="amount">The amount to pan the viewport.</param>
120+
/// <remarks>
121+
/// This method adjusts the current <see cref="ViewportLocation"/> incrementally based on the provided amount.
122+
/// It should only be called while a panning operation is active (see <see cref="BeginPanning(Point)"/>).
123+
/// </remarks>
124+
public void UpdatePanning(Vector amount)
125+
{
126+
Debug.Assert(IsPanning);
127+
ViewportLocation -= amount;
128+
}
129+
130+
/// <summary>
131+
/// Cancels the current panning operation and reverts the viewport to its initial location if <see cref="AllowPanningCancellation"/> is true.
132+
/// </summary>
133+
/// <remarks>This method has no effect if there's no panning operation in progress.</remarks>
134+
public void CancelPanning()
135+
{
136+
if (!AllowPanningCancellation || !IsPanning)
137+
{
138+
return;
139+
}
140+
141+
ViewportLocation = _initialPanningLocation;
142+
IsPanning = false;
143+
}
144+
145+
/// <summary>
146+
/// Ends the current panning operation, retaining the current <see cref="ViewportLocation"/>.
147+
/// </summary>
148+
/// <remarks>This method has no effect if there's no panning operation in progress.</remarks>
149+
public void EndPanning()
150+
{
151+
IsPanning = false;
152+
}
153+
154+
#region Auto panning
155+
156+
private void HandleAutoPanning(object? sender, EventArgs e)
157+
{
158+
if (!IsPanning && IsMouseCaptureWithin)
159+
{
160+
Point mousePosition = Mouse.GetPosition(this);
161+
double edgeDistance = AutoPanEdgeDistance;
162+
double autoPanSpeed = Math.Min(AutoPanSpeed, AutoPanSpeed * AutoPanningTickRate) / (ViewportZoom * 2);
163+
double x = ViewportLocation.X;
164+
double y = ViewportLocation.Y;
165+
166+
if (mousePosition.X <= edgeDistance)
167+
{
168+
x -= autoPanSpeed;
169+
}
170+
else if (mousePosition.X >= ActualWidth - edgeDistance)
171+
{
172+
x += autoPanSpeed;
173+
}
174+
175+
if (mousePosition.Y <= edgeDistance)
176+
{
177+
y -= autoPanSpeed;
178+
}
179+
else if (mousePosition.Y >= ActualHeight - edgeDistance)
180+
{
181+
y += autoPanSpeed;
182+
}
183+
184+
ViewportLocation = new Point(x, y);
185+
MouseLocation = Mouse.GetPosition(ItemsHost);
186+
187+
State.HandleAutoPanning(new MouseEventArgs(Mouse.PrimaryDevice, 0));
188+
}
189+
}
190+
191+
/// <summary>
192+
/// Called when the <see cref="DisableAutoPanning"/> changes.
193+
/// </summary>
194+
/// <param name="shouldDisable">Whether to enable or disable auto panning.</param>
195+
private void OnDisableAutoPanningChanged(bool shouldDisable)
196+
{
197+
if (shouldDisable)
198+
{
199+
_autoPanningTimer?.Stop();
200+
}
201+
else if (_autoPanningTimer == null)
202+
{
203+
_autoPanningTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(AutoPanningTickRate),
204+
DispatcherPriority.Background, HandleAutoPanning, Dispatcher);
205+
}
206+
else
207+
{
208+
_autoPanningTimer.Interval = TimeSpan.FromMilliseconds(AutoPanningTickRate);
209+
_autoPanningTimer.Start();
210+
}
211+
}
212+
213+
#endregion
214+
}
215+
}

0 commit comments

Comments
 (0)