Skip to content

Commit 243867c

Browse files
authored
Merge pull request #170 from miroiu/feature/146-input-system
* Refactored and improved input processing and state management * Added support for initiating and completing click-and-drag operations using the keyboard
2 parents 95d4bb4 + c1a354c commit 243867c

32 files changed

+1292
-949
lines changed

CHANGELOG.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
> - Renamed UnselectAllConnection to UnselectAllConnections in NodifyEditor
1212
> - Removed DragStarted, DragDelta and DragCompleted routed events from ItemContainer
1313
> - Replaced the System.Windows.Input.MouseGesture with Nodify.MouseGesture
14+
> - Removed State, GetInitialState, PushState, PopState and PopAllStates from NodifyEditor and ItemContainer
15+
> - Replaced EditorState and ContainerState with InputElementState
1416
> - Moved AllowCuttingCancellation from CuttingLine to NodifyEditor
1517
> - Moved AllowDraggingCancellation from ItemContainer to NodifyEditor
1618
> - Features:
@@ -19,18 +21,25 @@
1921
> - Added Select, BeginSelecting, UpdateSelection, EndSelecting, CancelSelecting and AllowSelectionCancellation to NodifyEditor
2022
> - Added IsDragging, BeginDragging, UpdateDragging, EndDragging and CancelDragging to NodifyEditor
2123
> - Added AlignSelection and AlignContainers methods to NodifyEditor
22-
> - Added HasCustomContextMenu dependency property to NodifyEditor and ItemContainer
24+
> - Added HasCustomContextMenu dependency property to NodifyEditor, ItemContainer and Connector
2325
> - Added Select, BeginDragging, UpdateDragging, EndDragging and CancelDragging to ItemContainer
2426
> - Added PreserveSelectionOnRightClick configuration field to ItemContainer
25-
> - Added State, GetInitialState, PushState, PopState and PopAllStates to Connector
2627
> - Added BeginConnecting, UpdatePendingConnection, EndConnecting, CancelConnecting and RemoveConnections methods to Connector
2728
> - Added a custom MouseGesture with support for key combinations
29+
> - Added InputProcessor to NodifyEditor, ItemContainer and Connector, enabling the extension of controls with custom states
30+
> - Added DragState to simplify creating click-and-drag operations, with support for initiating and completing them using the keyboard
31+
> - Added InputElementStateStack to manage transitions between states in UI elements
32+
> - Move the viewport to the mouse position when zooming on the Minimap if ResizeToViewport is false
2833
> - Bugfixes:
2934
> - Fixed an issue where the ItemContainer was selected by releasing the mouse button on it, even when the mouse was not captured
35+
> - Fixed an issue where the ItemContainer could open its context menu even when it was not selected
3036
> - Fixed an issue where the Home button caused the editor to fail to display items when contained within a ScrollViewer
3137
> - Fixed an issue where connector optimization did not work when SelectedItems was not data-bound
3238
> - Fixed EditorCommands.Align to perform a single arrange invalidation instead of one for each aligned container
39+
> - Fixed an issue where controls would capture the mouse unnecessarily; they now capture it only in response to a defined gesture
40+
> - Fixed an issue where the minimap could update the viewport without having the mouse captured
3341
> - Fixed ItemContainer.Select and NodifyEditor.SelectArea to clear the existing selection and select the containers within the same transaction
42+
> - Fixed an issue where editor operations failed to cancel upon losing mouse capture
3443
3544
#### **Version 6.6.0**
3645

Examples/Nodify.Calculator/EditorView.xaml.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,25 @@ public EditorView()
1010
{
1111
InitializeComponent();
1212

13-
EventManager.RegisterClassHandler(typeof(NodifyEditor), MouseLeftButtonDownEvent, new MouseButtonEventHandler(CloseOperationsMenu));
13+
EventManager.RegisterClassHandler(typeof(NodifyEditor), MouseLeftButtonDownEvent, new MouseButtonEventHandler(CloseOperationsMenu), true);
1414
EventManager.RegisterClassHandler(typeof(NodifyEditor), MouseRightButtonUpEvent, new MouseButtonEventHandler(OpenOperationsMenu));
1515
}
1616

1717
private void OpenOperationsMenu(object sender, MouseButtonEventArgs e)
1818
{
19-
if (!e.Handled && e.OriginalSource is NodifyEditor editor && editor.DataContext is CalculatorViewModel calculator)
19+
if (e.OriginalSource is NodifyEditor editor && editor.DataContext is CalculatorViewModel calculator)
2020
{
2121
e.Handled = true;
2222
calculator.OperationsMenu.OpenAt(editor.MouseLocation);
2323
}
2424
}
2525

26-
private void CloseOperationsMenu(object sender, RoutedEventArgs e)
26+
private void CloseOperationsMenu(object sender, MouseButtonEventArgs e)
2727
{
2828
ItemContainer? itemContainer = sender as ItemContainer;
2929
NodifyEditor? editor = sender as NodifyEditor ?? itemContainer?.Editor;
3030

31-
if (!e.Handled && editor?.DataContext is CalculatorViewModel calculator)
31+
if (editor?.DataContext is CalculatorViewModel calculator)
3232
{
3333
calculator.OperationsMenu.Close();
3434
}

Examples/Nodify.Calculator/OperationsMenuView.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<Button Content="{Binding Title}"
3838
Command="{Binding DataContext.CreateOperationCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
3939
CommandParameter="{Binding}"
40+
ClickMode="Press"
4041
Background="Transparent"
4142
BorderBrush="Transparent"
4243
Foreground="{DynamicResource ForegroundBrush}"

Examples/Nodify.StateMachine/StateMachineViewModel.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,16 @@ public StateMachineViewModel()
1717
Conditions = new NodifyObservableCollection<BlackboardItemReferenceViewModel>(BlackboardDescriptor.GetAvailableItems<IBlackboardCondition>())
1818
};
1919

20-
Transitions.WhenAdded(c => c.Source.Transitions.Add(c.Target))
21-
.WhenRemoved(c => c.Source.Transitions.Remove(c.Target))
20+
Transitions.WhenAdded(c =>
21+
{
22+
c.Source.Transitions.Add(c.Target);
23+
c.Target.Transitions.Add(c.Source);
24+
})
25+
.WhenRemoved(c =>
26+
{
27+
c.Source.Transitions.Remove(c.Target);
28+
c.Target.Transitions.Remove(c.Source);
29+
})
2230
.WhenCleared(c => c.ForEach(i =>
2331
{
2432
i.Source.Transitions.Clear();

Nodify/Connections/BaseConnection.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -774,8 +774,6 @@ protected override void OnMouseDown(MouseButtonEventArgs e)
774774
{
775775
Focus();
776776

777-
this.CaptureMouseSafe();
778-
779777
EditorGestures.ConnectionGestures gestures = EditorGestures.Mappings.Connection;
780778
if (gestures.Split.Matches(e.Source, e))
781779
{
@@ -827,14 +825,6 @@ protected internal void OnDisconnect()
827825
}
828826
}
829827

830-
protected override void OnMouseUp(MouseButtonEventArgs e)
831-
{
832-
if (IsMouseCaptured)
833-
{
834-
ReleaseMouseCapture();
835-
}
836-
}
837-
838828
private Pen GetOutlinePen()
839829
{
840830
return _outlinePen ??= new Pen(OutlineBrush, StrokeThickness + OutlineThickness * 2d);

Nodify/Connections/Connector.cs

Lines changed: 70 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System.Collections.Generic;
2-
using System.Diagnostics;
1+
using System.Diagnostics;
32
using System.Windows;
43
using System.Windows.Controls;
54
using System.Windows.Input;
@@ -61,6 +60,7 @@ public event ConnectorEventHandler Disconnect
6160
public static readonly DependencyProperty DisconnectCommandProperty = DependencyProperty.Register(nameof(DisconnectCommand), typeof(ICommand), typeof(Connector));
6261
private static readonly DependencyPropertyKey IsPendingConnectionPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsPendingConnection), typeof(bool), typeof(Connector), new FrameworkPropertyMetadata(BoxValue.False));
6362
public static readonly DependencyProperty IsPendingConnectionProperty = IsPendingConnectionPropertyKey.DependencyProperty;
63+
public static readonly DependencyProperty HasCustomContextMenuProperty = NodifyEditor.HasCustomContextMenuProperty.AddOwner(typeof(Connector));
6464

6565
/// <summary>
6666
/// Gets the location in graph space coordinates where <see cref="Connection"/>s can be attached to.
@@ -100,18 +100,22 @@ public ICommand? DisconnectCommand
100100
set => SetValue(DisconnectCommandProperty, value);
101101
}
102102

103-
#endregion
104-
105-
static Connector()
103+
/// <summary>
104+
/// Gets or sets a value indicating whether the connector uses a custom context menu.
105+
/// </summary>
106+
/// <remarks>When set to true, the connector handles the right-click event for specific operations.</remarks>
107+
public bool HasCustomContextMenu
106108
{
107-
DefaultStyleKeyProperty.OverrideMetadata(typeof(Connector), new FrameworkPropertyMetadata(typeof(Connector)));
108-
FocusableProperty.OverrideMetadata(typeof(Connector), new FrameworkPropertyMetadata(BoxValue.True));
109+
get => (bool)GetValue(HasCustomContextMenuProperty);
110+
set => SetValue(HasCustomContextMenuProperty, value);
109111
}
110112

111-
public Connector()
112-
{
113-
_states.Push(GetInitialState());
114-
}
113+
/// <summary>
114+
/// Gets a value indicating whether the connector has a context menu.
115+
/// </summary>
116+
public bool HasContextMenu => ContextMenu != null || HasCustomContextMenu;
117+
118+
#endregion
115119

116120
#region Fields
117121

@@ -162,6 +166,19 @@ public Connector()
162166

163167
#endregion
164168

169+
static Connector()
170+
{
171+
DefaultStyleKeyProperty.OverrideMetadata(typeof(Connector), new FrameworkPropertyMetadata(typeof(Connector)));
172+
FocusableProperty.OverrideMetadata(typeof(Connector), new FrameworkPropertyMetadata(BoxValue.True));
173+
}
174+
175+
public Connector()
176+
{
177+
InputProcessor.AddHandler(new ConnectorDisconnectState(this));
178+
InputProcessor.AddHandler(new ConnectorConnectingState(this));
179+
InputProcessor.AddHandler(new ConnectorDefaultState(this));
180+
}
181+
165182
/// <inheritdoc />
166183
public override void OnApplyTemplate()
167184
{
@@ -314,68 +331,18 @@ public void UpdateAnchor()
314331

315332
#endregion
316333

317-
#region State Handling
318-
319-
private readonly Stack<ConnectorState> _states = new Stack<ConnectorState>();
320-
321-
/// <summary>The current state of the connector.</summary>
322-
public ConnectorState State => _states.Peek();
323-
324-
/// <summary>Creates the initial state of the connector.</summary>
325-
/// <returns>The initial state.</returns>
326-
protected virtual ConnectorState GetInitialState()
327-
=> new ConnectorDefaultState(this);
328-
329-
/// <summary>Pushes the given state to the stack.</summary>
330-
/// <param name="state">The new state of the connector.</param>
331-
/// <remarks>Calls <see cref="ConnectorState.Enter"/> on the new state.</remarks>
332-
public void PushState(ConnectorState state)
333-
{
334-
var prev = State;
335-
_states.Push(state);
336-
state.Enter(prev);
337-
}
338-
339-
/// <summary>Pops the current <see cref="State"/> from the stack.</summary>
340-
/// <remarks>It doesn't pop the initial state. (see <see cref="GetInitialState"/>)
341-
/// <br />Calls <see cref="ConnectorState.Exit"/> on the current state.
342-
/// <br />Calls <see cref="ConnectorState.ReEnter"/> on the previous state.
343-
/// </remarks>
344-
public void PopState()
345-
{
346-
// Never remove the default state
347-
if (_states.Count > 1)
348-
{
349-
ConnectorState prev = _states.Pop();
350-
prev.Exit();
351-
State.ReEnter(prev);
352-
}
353-
}
334+
#region Gesture Handling
354335

355-
/// <summary>Pops all states from the connector.</summary>
356-
/// <remarks>It doesn't pop the initial state. (see <see cref="GetInitialState"/>)</remarks>
357-
public void PopAllStates()
358-
{
359-
while (_states.Count > 1)
360-
{
361-
PopState();
362-
}
363-
}
336+
protected InputProcessor InputProcessor { get; } = new InputProcessor { ProcessHandledEvents = true };
364337

365338
/// <inheritdoc />
366339
protected override void OnMouseDown(MouseButtonEventArgs e)
367-
{
368-
Focus();
369-
370-
this.CaptureMouseSafe();
371-
372-
State.HandleMouseDown(e);
373-
}
340+
=> InputProcessor.Process(e);
374341

375342
/// <inheritdoc />
376343
protected override void OnMouseUp(MouseButtonEventArgs e)
377344
{
378-
State.HandleMouseUp(e);
345+
InputProcessor.Process(e);
379346

380347
// Release the mouse capture if all the mouse buttons are released and there's no sticky connection pending
381348
if (!IsPendingConnection && IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released)
@@ -386,21 +353,38 @@ protected override void OnMouseUp(MouseButtonEventArgs e)
386353

387354
/// <inheritdoc />
388355
protected override void OnMouseMove(MouseEventArgs e)
389-
=> State.HandleMouseMove(e);
356+
=> InputProcessor.Process(e);
390357

391358
/// <inheritdoc />
392359
protected override void OnMouseWheel(MouseWheelEventArgs e)
393-
=> State.HandleMouseWheel(e);
360+
=> InputProcessor.Process(e);
394361

395362
/// <inheritdoc />
396363
protected override void OnLostMouseCapture(MouseEventArgs e)
397-
=> PopAllStates();
364+
=> InputProcessor.Process(e);
398365

366+
/// <inheritdoc />
399367
protected override void OnKeyUp(KeyEventArgs e)
400-
=> State.HandleKeyUp(e);
368+
{
369+
InputProcessor.Process(e);
401370

371+
if (!IsPendingConnection && IsMouseCaptured)
372+
{
373+
ReleaseMouseCapture();
374+
}
375+
}
376+
377+
/// <inheritdoc />
402378
protected override void OnKeyDown(KeyEventArgs e)
403-
=> State.HandleKeyDown(e);
379+
{
380+
InputProcessor.Process(e);
381+
382+
// Release the mouse capture if all the mouse buttons are released and there's no sticky connection pending
383+
if (!IsPendingConnection && IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released)
384+
{
385+
ReleaseMouseCapture();
386+
}
387+
}
404388

405389
#endregion
406390

@@ -468,26 +452,31 @@ public void UpdatePendingConnection(Point position)
468452
}
469453

470454
/// <summary>
471-
/// Cancels the current pending connection without completing it.
455+
/// Cancels the current pending connection without completing it if <see cref="AllowPendingConnectionCancellation"/> is true.
456+
/// Otherwise, it completes the pending connection by calling <see cref="EndConnecting()"/>.
472457
/// </summary>
473458
/// <remarks>This method has no effect if there's no pending connection.</remarks>
474459
public void CancelConnecting()
475460
{
476-
if (!IsPendingConnection)
461+
if (!AllowPendingConnectionCancellation)
477462
{
463+
EndConnecting();
478464
return;
479465
}
480466

481-
var args = new PendingConnectionEventArgs(DataContext)
467+
if (IsPendingConnection)
482468
{
483-
RoutedEvent = PendingConnectionCompletedEvent,
484-
Anchor = Anchor,
485-
Source = this,
486-
Canceled = true
487-
};
488-
RaiseEvent(args);
489-
490-
IsPendingConnection = false;
469+
var args = new PendingConnectionEventArgs(DataContext)
470+
{
471+
RoutedEvent = PendingConnectionCompletedEvent,
472+
Anchor = Anchor,
473+
Source = this,
474+
Canceled = true
475+
};
476+
RaiseEvent(args);
477+
478+
IsPendingConnection = false;
479+
}
491480
}
492481

493482
/// <summary>

0 commit comments

Comments
 (0)