diff --git a/CHANGELOG.md b/CHANGELOG.md index 26fd9742..b33b806b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,13 +11,13 @@ > - Renamed PushItems to UpdatePushedArea and StartPushingItems to BeginPushingItems in NodifyEditor > - Renamed UnselectAllConnection to UnselectAllConnections in NodifyEditor > - Removed DragStarted, DragDelta and DragCompleted routed events from ItemContainer -> - Replaced the System.Windows.Input.MouseGesture with Nodify.MouseGesture +> - Replaced the System.Windows.Input.MouseGesture with Nodify.Interactivity.MouseGesture for default EditorGesture mappings > - Removed State, GetInitialState, PushState, PopState and PopAllStates from NodifyEditor and ItemContainer > - Replaced EditorState and ContainerState with InputElementState > - Moved AllowCuttingCancellation from CuttingLine to NodifyEditor > - Moved AllowDraggingCancellation from ItemContainer to NodifyEditor > - Moved EditorGestures under the Nodify.Interactivity namespace -> - Moved Editor events under the Nodify.Events namespace +> - Moved editor events under the Nodify.Events namespace > - Features: > - Added BeginPanning, UpdatePanning, EndPanning, CancelPanning and AllowPanningCancellation to NodifyEditor and Minimap > - Added MouseLocation, ZoomAtPosition and GetLocationInsideMinimap to Minimap @@ -34,8 +34,8 @@ > - Added FindTargetConnector and FindConnectionTarget methods to Connector > - Added a custom MouseGesture with support for key combinations > - Added InputProcessor to NodifyEditor, ItemContainer, Connector, BaseConnection and Minimap, enabling the extension of controls with custom states -> - Added DragState to simplify creating click-and-drag operations, with support for initiating and completing them using the keyboard -> - Added InputElementStateStack to manage transitions between states in UI elements +> - Added DragState to simplify creating click-and-drag interactions, with support for initiating and completing them using the keyboard +> - Added InputElementStateStack, InputElementStateStack.DragState and InputElementStateStack.InputElementState to manage transitions between states in UI elements > - Added InputProcessor.Shared to enable the addition of global input handlers > - Move the viewport to the mouse position when zooming on the Minimap if ResizeToViewport is false > - Added SplitAtLocation and Remove methods to BaseConnection @@ -43,6 +43,7 @@ > - Added AllowZoomingWhilePanning, AllowZoomingWhileSelecting, AllowZoomingWhileCutting and AllowZoomingWhilePushingItems to EditorState > - Added EnableToggledSelectingMode, EnableToggledPanningMode, EnableToggledPushingItemsMode and EnableToggledCuttingMode to EditorState > - Added MinimapState.EnableToggledPanningMode +> - Added ContainerState.EnableToggledDraggingMode > - Added Unbind to InputGestureRef and EditorGestures.SelectionGestures > - Added EnableHitTesting to PendingConnection > - Bugfixes: @@ -54,7 +55,8 @@ > - Fixed an issue where controls would capture the mouse unnecessarily; they now capture it only in response to a defined gesture > - Fixed an issue where the minimap could update the viewport without having the mouse captured > - Fixed ItemContainer.Select and NodifyEditor.SelectArea to clear the existing selection and select the containers within the same transaction -> - Fixed an issue where editor operations failed to cancel upon losing mouse capture +> - Fixed an issue where editor interactions failed to cancel upon losing mouse capture +> - Fixed an issue where selecting a new connection would not clear the previous selection within the same transaction #### **Version 6.6.0** diff --git a/Examples/Nodify.Calculator/EditorView.xaml b/Examples/Nodify.Calculator/EditorView.xaml index 8ed7bdb2..24f923a0 100644 --- a/Examples/Nodify.Calculator/EditorView.xaml +++ b/Examples/Nodify.Calculator/EditorView.xaml @@ -209,7 +209,8 @@ - + - + diff --git a/Examples/Nodify.Playground/Editor/NodifyEditorView.xaml.cs b/Examples/Nodify.Playground/Editor/NodifyEditorView.xaml.cs index c2c36a7c..6867bfe8 100644 --- a/Examples/Nodify.Playground/Editor/NodifyEditorView.xaml.cs +++ b/Examples/Nodify.Playground/Editor/NodifyEditorView.xaml.cs @@ -15,6 +15,7 @@ public NodifyEditorView() static NodifyEditorView() { + InputProcessor.Shared.ReplaceHandlerFactory(elem => new CustomConnecting(elem)); InputProcessor.Shared.RegisterHandlerFactory(elem => new RetargetConnections(elem)); } diff --git a/Examples/Nodify.Playground/Editor/RetargetConnections.cs b/Examples/Nodify.Playground/Editor/RetargetConnections.cs index 8d16a1d8..1dbbc97e 100644 --- a/Examples/Nodify.Playground/Editor/RetargetConnections.cs +++ b/Examples/Nodify.Playground/Editor/RetargetConnections.cs @@ -5,14 +5,35 @@ namespace Nodify.Playground { + /// + /// Connecting state that prevents connecting when is in progress. + /// + public class CustomConnecting : ConnectorState.Connecting + { + protected override bool CanBegin => !RetargetConnections.InProgress; + + public CustomConnecting(Connector connector) : base(connector) + { + } + } + /// /// Hold CTRL+LeftClick on a connector to start reconnecting it. /// public class RetargetConnections : DragState { - public static InputGestureRef Reconnect { get; } = new Interactivity.MouseGesture(MouseAction.LeftClick, ModifierKeys.Control); + public static InputGestureRef Reconnect { get; } = new Interactivity.MouseGesture(MouseAction.LeftClick, ModifierKeys.Control) + { + IgnoreModifierKeysOnRelease = true + }; + + /// + /// Used to prevent connecting when is enabled. + /// + public static bool InProgress { get; private set; } protected override bool CanBegin => ViewModel.IsConnected && ViewModel.Flow == ConnectorFlow.Input; + protected override bool IsToggle => EditorSettings.Instance.EnableStickyConnectors; private ConnectorViewModel ViewModel => (ConnectorViewModel)Element.DataContext; private Vector _connectorOffset; @@ -20,6 +41,7 @@ public class RetargetConnections : DragState public RetargetConnections(Connector element) : base(element, Reconnect, EditorGestures.Mappings.Connector.CancelAction) { + PositionElement = Element.Editor ?? (IInputElement)Element; } protected override void OnBegin(InputEventArgs e) @@ -27,6 +49,8 @@ protected override void OnBegin(InputEventArgs e) _connectorOffset = ViewModel.Node.Orientation == Orientation.Horizontal ? (Vector)EditorSettings.Instance.ConnectionTargetOffset.Value : new Vector(EditorSettings.Instance.ConnectionTargetOffset.Value.Y, EditorSettings.Instance.ConnectionTargetOffset.Value.X); + + InProgress = true; } protected override void OnMouseMove(MouseEventArgs e) @@ -70,6 +94,7 @@ protected override void OnEnd(InputEventArgs e) // Reset the position of connections that were not rewired Element.UpdateAnchor(); + InProgress = false; } protected override void OnCancel(InputEventArgs e) @@ -78,6 +103,7 @@ protected override void OnCancel(InputEventArgs e) // Reset the position of connections that were not rewired Element.UpdateAnchor(); + InProgress = false; } /// diff --git a/Examples/Nodify.Playground/EditorSettings.cs b/Examples/Nodify.Playground/EditorSettings.cs index 79f457ad..aac5d31b 100644 --- a/Examples/Nodify.Playground/EditorSettings.cs +++ b/Examples/Nodify.Playground/EditorSettings.cs @@ -341,6 +341,11 @@ private EditorSettings() val => Instance.EnableToggledSelecting = val, "Enable toggled selecting mode: ", "The interaction will be completed in two steps using the same gesture to start and end."), + new ProxySettingViewModel( + () => Instance.EnableToggledDragging, + val => Instance.EnableToggledDragging = val, + "Enable toggled dragging mode: ", + "The interaction will be completed in two steps using the same gesture to start and end."), new ProxySettingViewModel( () => Instance.EnableMinimapToggledPanning, val => Instance.EnableMinimapToggledPanning = val, @@ -832,6 +837,12 @@ public bool EnableToggledSelecting set => EditorState.EnableToggledSelectingMode = value; } + public bool EnableToggledDragging + { + get => ContainerState.EnableToggledDraggingMode; + set => ContainerState.EnableToggledDraggingMode = value; + } + public bool EnableMinimapToggledPanning { get => MinimapState.EnableToggledPanningMode; diff --git a/Nodify/Connections/BaseConnection.cs b/Nodify/Connections/BaseConnection.cs index 97a39bc8..792318ee 100644 --- a/Nodify/Connections/BaseConnection.cs +++ b/Nodify/Connections/BaseConnection.cs @@ -811,7 +811,7 @@ protected override void OnMouseUp(MouseButtonEventArgs e) InputProcessor.ProcessEvent(e); // Release the mouse capture if all the mouse buttons are released - if (IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released) + if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released) { ReleaseMouseCapture(); } @@ -835,7 +835,7 @@ protected override void OnKeyUp(KeyEventArgs e) InputProcessor.ProcessEvent(e); // Release the mouse capture if all the mouse buttons are released - if (IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released) + if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released) { ReleaseMouseCapture(); } diff --git a/Nodify/Connectors/Connector.cs b/Nodify/Connectors/Connector.cs index 63e6a787..62a67ec0 100644 --- a/Nodify/Connectors/Connector.cs +++ b/Nodify/Connectors/Connector.cs @@ -328,7 +328,7 @@ public void UpdateAnchor() #region Gesture Handling - protected InputProcessor InputProcessor { get; } = new InputProcessor { ProcessHandledEvents = true }; + protected InputProcessor InputProcessor { get; } = new InputProcessor(); /// protected override void OnMouseDown(MouseButtonEventArgs e) @@ -340,7 +340,7 @@ protected override void OnMouseUp(MouseButtonEventArgs e) InputProcessor.ProcessEvent(e); // Release the mouse capture if all the mouse buttons are released and there's no interaction in progress - if (IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released && !IsToggledInteractionInProgress()) + if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released) { ReleaseMouseCapture(); } @@ -364,7 +364,7 @@ protected override void OnKeyUp(KeyEventArgs e) InputProcessor.ProcessEvent(e); // Release the mouse capture if all the mouse buttons are released and there's no interaction in progress - if (IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released && !IsToggledInteractionInProgress()) + if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released) { ReleaseMouseCapture(); } @@ -374,14 +374,6 @@ protected override void OnKeyUp(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e) => InputProcessor.ProcessEvent(e); - /// - /// Determines whether any toggled interaction is currently in progress. - /// - protected virtual bool IsToggledInteractionInProgress() - { - return ConnectorState.EnableToggledConnectingMode && IsPendingConnection; - } - #endregion #region Methods diff --git a/Nodify/Containers/ItemContainer.cs b/Nodify/Containers/ItemContainer.cs index 0ff12096..831ce8ee 100644 --- a/Nodify/Containers/ItemContainer.cs +++ b/Nodify/Containers/ItemContainer.cs @@ -372,7 +372,7 @@ protected override void OnMouseUp(MouseButtonEventArgs e) InputProcessor.ProcessEvent(e); // Release the mouse capture if all the mouse buttons are released and there's no interaction in progress - if (IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released && !IsToggledInteractionInProgress()) + if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released) { ReleaseMouseCapture(); } @@ -396,7 +396,7 @@ protected override void OnKeyUp(KeyEventArgs e) InputProcessor.ProcessEvent(e); // Release the mouse capture if all the mouse buttons are released and there's no interaction in progress - if (IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released && !IsToggledInteractionInProgress()) + if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released) { ReleaseMouseCapture(); } @@ -406,11 +406,6 @@ protected override void OnKeyUp(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e) => InputProcessor.ProcessEvent(e); - /// - /// Determines whether any toggled interaction is currently in progress. - /// - protected virtual bool IsToggledInteractionInProgress() => false; - #endregion } } diff --git a/Nodify/Containers/States/ContainerState.cs b/Nodify/Containers/States/ContainerState.cs index f784c2eb..308bcc56 100644 --- a/Nodify/Containers/States/ContainerState.cs +++ b/Nodify/Containers/States/ContainerState.cs @@ -2,6 +2,11 @@ { public static partial class ContainerState { + /// + /// Determines whether toggled dragging mode is enabled, allowing the user to start and end the interaction in two steps with the same input gesture. + /// + public static bool EnableToggledDraggingMode { get; set; } + internal static void RegisterDefaultHandlers() { InputProcessor.Shared.RegisterHandlerFactory(elem => new Default(elem)); diff --git a/Nodify/Containers/States/Dragging.cs b/Nodify/Containers/States/Dragging.cs index 53dc6c99..3c967e63 100644 --- a/Nodify/Containers/States/Dragging.cs +++ b/Nodify/Containers/States/Dragging.cs @@ -9,6 +9,7 @@ public static partial class ContainerState internal sealed class Dragging : InputElementStateStack.DragState { protected override bool CanCancel => NodifyEditor.AllowDraggingCancellation; + protected override bool IsToggle => EnableToggledDraggingMode; private Point _previousMousePosition; @@ -20,13 +21,12 @@ public Dragging(InputElementStateStack stack) PositionElement = Element.Editor; } - protected override void OnBegin(InputElementStateStack.IInputElementState? from) + protected override void OnBegin(InputEventArgs e) { _previousMousePosition = Element.Editor.MouseLocation; Element.BeginDragging(); } - /// protected override void OnMouseMove(MouseEventArgs e) { Element.UpdateDragging(Element.Editor.MouseLocation - _previousMousePosition); diff --git a/Nodify/Editor/NodifyEditor.cs b/Nodify/Editor/NodifyEditor.cs index f52188a5..a9ef554c 100644 --- a/Nodify/Editor/NodifyEditor.cs +++ b/Nodify/Editor/NodifyEditor.cs @@ -831,7 +831,7 @@ protected override void OnMouseUp(MouseButtonEventArgs e) InputProcessor.ProcessEvent(e); // Release the mouse capture if all the mouse buttons are released and there's no interaction in progress - if (IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released && !IsToggledInteractionInProgress()) + if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released) { ReleaseMouseCapture(); } @@ -861,7 +861,7 @@ protected override void OnKeyUp(KeyEventArgs e) InputProcessor.ProcessEvent(e); // Release the mouse capture if all the mouse buttons are released and there's no interaction in progress - if (IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released && !IsToggledInteractionInProgress()) + if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released) { ReleaseMouseCapture(); } @@ -871,17 +871,6 @@ protected override void OnKeyUp(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e) => InputProcessor.ProcessEvent(e); - /// - /// Determines whether any toggled interaction is currently in progress. - /// - protected virtual bool IsToggledInteractionInProgress() - { - return EditorState.EnableToggledPanningMode && IsPanning - || EditorState.EnableToggledSelectingMode && IsSelecting - || EditorState.EnableToggledPushingItemsMode && IsPushingItems - || EditorState.EnableToggledCuttingMode && IsCutting; - } - #endregion /// diff --git a/Nodify/Interactivity/DragState.cs b/Nodify/Interactivity/DragState.cs index aa5f5ae1..462d3420 100644 --- a/Nodify/Interactivity/DragState.cs +++ b/Nodify/Interactivity/DragState.cs @@ -116,8 +116,26 @@ void IInputHandler.HandleEvent(InputEventArgs e) #region Interaction logic + // Begin the interaction on gesture press + private bool TryBeginDragging(InputEventArgs e) + { + if (IsInputEventPressed(e) && CanBegin && BeginGesture.Matches(e.Source, e)) + { + BeginDrag(e); + return true; + } + + return false; + } + private bool TryEndDragging(InputEventArgs e) { + if (IsInputCaptureLost(e)) + { + EndDrag(e); + return true; + } + if (IsToggle && _interactionState == InteractionState.InProgress) { return TryDeferToggleInteractionEnd(e); @@ -139,18 +157,6 @@ private bool TryDeferToggleInteractionEnd(InputEventArgs e) return false; } - // Begin the interaction on gesture press - private bool TryBeginDragging(InputEventArgs e) - { - if (IsInputEventPressed(e) && CanBegin && BeginGesture.Matches(e.Source, e)) - { - BeginDrag(e); - return true; - } - - return false; - } - // End the interaction on gesture release private bool TryEndInteraction(InputEventArgs e) { @@ -166,7 +172,7 @@ private bool TryEndInteraction(InputEventArgs e) // Cancel the interaction private bool TryCancelDragging(InputEventArgs e) { - if (IsInputCaptureLost(e) || CanCancel && IsInputEventReleased(e) && CancelGesture?.Matches(e.Source, e) is true) + if (CanCancel && IsInputEventReleased(e) && CancelGesture?.Matches(e.Source, e) is true) { CancelDrag(e); return true; @@ -196,22 +202,21 @@ private void TryHandleEvent(InputEventArgs e) } } - private void BeginDrag(InputEventArgs e) + internal void BeginDrag(InputEventArgs e) { // Avoid stealing mouse capture from other elements - if (IsInputCaptured(e)) + if (CanCaptureInput(e)) { + RequiresInputCapture = IsToggle; + _interactionState = InteractionState.InProgress; + _initialPosition = GetInitialPosition(e); + HandleEvent(e); // Handle the event, otherwise CaptureMouse will send a MouseMove event and the current event will be handled out of order OnBegin(e); e.Handled = true; - if (e is MouseEventArgs me) - { - _initialPosition = me.GetPosition(PositionElement); - } - Element.Focus(); CaptureInput(e); } @@ -243,6 +248,8 @@ private void EndDrag(InputEventArgs e) OnEnd(e); e.Handled = true; } + + RequiresInputCapture = false; } private void CancelDrag(InputEventArgs e) @@ -252,20 +259,51 @@ private void CancelDrag(InputEventArgs e) OnCancel(e); e.Handled = true; + RequiresInputCapture = false; } #endregion - protected virtual bool IsInputCaptured(InputEventArgs e) + /// + /// Retrieves the initial position of the input event relative to the . + /// + /// The representing the input event. + /// + /// This position is used to calculate the drag distance, to determine whether + /// the context menu can appear or if the action is considered a drag operation. The behavior is influenced + /// by the . + /// + protected virtual Point GetInitialPosition(InputEventArgs e) + { + if (e is MouseEventArgs me) + { + return me.GetPosition(PositionElement); + } + + return default; + } + + /// + /// Determines whether input capture can be acquired for the . + /// + /// The representing the input event. + /// Must return true if the input is already captured by the current element. + protected virtual bool CanCaptureInput(InputEventArgs e) => Mouse.Captured == null || Element.IsMouseCaptured; + /// + /// Captures input for the element. + /// + /// The representing the input event. protected virtual void CaptureInput(InputEventArgs e) => Element.CaptureMouse(); + /// + /// Determines whether input capture has been lost. + /// + /// The representing the input event. protected virtual bool IsInputCaptureLost(InputEventArgs e) - { - return e.RoutedEvent == UIElement.LostMouseCaptureEvent; - } + => e.RoutedEvent == UIElement.LostMouseCaptureEvent; /// /// Determines if the given input event represents the release of an input gesture. diff --git a/Nodify/Interactivity/IInputHandler.cs b/Nodify/Interactivity/IInputHandler.cs index 9d27d2b7..3b56a2a6 100644 --- a/Nodify/Interactivity/IInputHandler.cs +++ b/Nodify/Interactivity/IInputHandler.cs @@ -11,6 +11,19 @@ public interface IInputHandler /// Handles a given input event, such as a mouse or keyboard interaction. /// /// The representing the input event. + /// + /// This method is invoked when an input event is dispatched to the handler. Implementations should + /// handle the event logic and optionally mark the event as handled. + /// void HandleEvent(InputEventArgs e); + + /// + /// Gets a value indicating whether the handler requires input capture to remain active. + /// + /// + /// This property can be used to determine whether it is safe to release mouse capture, especially during toggled interactions.
+ /// Toggled interactions usually involve two steps, and it is important to keep the input capture active until the interaction is completed. + ///
+ bool RequiresInputCapture { get; } } } diff --git a/Nodify/Interactivity/InputElementState.cs b/Nodify/Interactivity/InputElementState.cs index 3ec4cacf..00365816 100644 --- a/Nodify/Interactivity/InputElementState.cs +++ b/Nodify/Interactivity/InputElementState.cs @@ -15,6 +15,8 @@ public abstract class InputElementState : IInputHandler ///
protected TElement Element { get; } + public bool RequiresInputCapture { get; protected set; } + /// /// Initializes a new instance of the class. /// diff --git a/Nodify/Interactivity/InputElementStateStack.DragState.cs b/Nodify/Interactivity/InputElementStateStack.DragState.cs index 3f3718f4..00107dea 100644 --- a/Nodify/Interactivity/InputElementStateStack.DragState.cs +++ b/Nodify/Interactivity/InputElementStateStack.DragState.cs @@ -8,45 +8,29 @@ public partial class InputElementStateStack where TElement : Framework /// /// Represents a specialized state for handling drag interactions. /// - public abstract class DragState : InputElementState, IInputHandler + public abstract class DragState : DragState, IInputElementState, IInputHandler { /// - /// The gesture used to exit the drag state. + /// Gets the state stack managing this state. /// - protected InputGesture ExitGesture { get; } + public InputElementStateStack Stack { get; } - /// - /// The gesture used to cancel the drag state, if supported. - /// - protected InputGesture? CancelGesture { get; } - - /// - /// Gets or sets whether the element has a context menu. - /// - protected virtual bool HasContextMenu => Element.ContextMenu != null; - - /// - /// Gets or sets whether the drag interaction can be canceled. - /// - protected virtual bool CanCancel { get; } = true; - - /// - /// Gets or sets the element used for position calculations. - /// - protected IInputElement PositionElement { get; set; } - - private bool _canReceiveInput; - private Point _initialPosition; + private readonly InputEventArgs _mouseEventArgs = new MouseEventArgs(Mouse.PrimaryDevice, 0, Stylus.CurrentStylusDevice) + { + RoutedEvent = NodifyEditor.ViewportUpdatedEvent // dummy event + }; /// /// Initializes a new instance of the class. /// /// The state stack managing this state. /// The gesture used to exit the drag state. - public DragState(InputElementStateStack stack, InputGesture exitGesture) : base(stack) + /// The gesture used to cancel the drag state. + public DragState(InputElementStateStack stack, InputGesture exitGesture, InputGesture cancelGesture) + : base(stack.Element, exitGesture, cancelGesture) { - ExitGesture = exitGesture; PositionElement = stack.Element; + Stack = stack; } /// @@ -54,140 +38,38 @@ public DragState(InputElementStateStack stack, InputGesture exitGestur /// /// The state stack managing this state. /// The gesture used to exit the drag state. - /// The gesture used to cancel the drag state. - public DragState(InputElementStateStack stack, InputGesture exitGesture, InputGesture cancelGesture) - : this(stack, exitGesture) - { - CancelGesture = cancelGesture; - } - - public sealed override void Enter(IInputElementState? from) - { - if (Mouse.Captured == null || Element.IsMouseCaptured) - { - _initialPosition = new Point(); - _canReceiveInput = true; - OnBegin(from); - - Element.Focus(); - Element.CaptureMouse(); - } - } - - public sealed override void Exit() + public DragState(InputElementStateStack stack, InputGesture exitGesture) + : base(stack.Element, exitGesture) { + PositionElement = stack.Element; + Stack = stack; } - void IInputHandler.HandleEvent(InputEventArgs e) - { - if (e is MouseEventArgs me && _initialPosition == new Point()) - { - _initialPosition = me.GetPosition(PositionElement); - } - - if (_canReceiveInput && IsInputEventReleased(e) && ExitGesture.Matches(e.Source, e)) - { - EndDrag(e); - return; - } - - if (_canReceiveInput && (e.RoutedEvent == UIElement.LostMouseCaptureEvent || CanCancel && CancelGesture?.Matches(e.Source, e) is true && IsInputEventReleased(e))) - { - CancelDrag(e); - return; - } - - if (_canReceiveInput) - { - HandleEvent(e); - } - } - - private void CancelDrag(InputEventArgs e) - { - _canReceiveInput = false; - - HandleEvent(e); - OnCancel(e); - - e.Handled = true; - - PopState(); - } + public void Enter(IInputElementState? from) + => BeginDrag(_mouseEventArgs); - private void EndDrag(InputEventArgs e) + public void Exit() { - _canReceiveInput = false; - - HandleEvent(e); - - // Suppress the context menu if the mouse moved beyond the defined drag threshold - if (e is MouseButtonEventArgs mbe && mbe.ChangedButton == MouseButton.Right && HasContextMenu) - { - double dragThreshold = NodifyEditor.MouseActionSuppressionThreshold * NodifyEditor.MouseActionSuppressionThreshold; - double dragDistance = (mbe.GetPosition(PositionElement) - _initialPosition).LengthSquared; - - if (dragDistance > dragThreshold) - { - OnEnd(e); - e.Handled = true; - } - else - { - OnCancel(e); - } - } - else - { - OnEnd(e); - e.Handled = true; - } - - PopState(); } /// - /// Determines if the given input event represents the release of an input gesture. + /// Pushes a new state onto the stack. /// - /// The input event to evaluate. - /// True if the event represents the release of a gesture; otherwise, false. - protected virtual bool IsInputEventReleased(InputEventArgs e) - { - if (e is MouseButtonEventArgs mbe && mbe.ButtonState == MouseButtonState.Released) - return true; - - if (e is KeyEventArgs ke && ke.IsUp) - return true; - - if (e is MouseWheelEventArgs mwe && mwe.MiddleButton == MouseButtonState.Released) - return true; - - return false; - } + /// The new state to push. + public void PushState(IInputElementState newState) + => Stack.PushState(newState); /// - /// Called when the drag interaction begins. Override to provide custom behavior. + /// Pops the current state from the stack. /// - /// The input event that started the interaction. - protected virtual void OnBegin(IInputElementState? from) - { - } + public void PopState() + => Stack.PopState(); - /// - /// Called when the drag interaction ends. Override to provide custom behavior. - /// - /// The input event that ended the interaction. - protected virtual void OnEnd(InputEventArgs e) - { - } + protected override void OnCancel(InputEventArgs e) + => PopState(); - /// - /// Called when the drag interaction is canceled. Override to provide custom behavior. - /// - /// The input event that canceled the interaction. - protected virtual void OnCancel(InputEventArgs e) - { - } + protected override void OnEnd(InputEventArgs e) + => PopState(); } } } diff --git a/Nodify/Interactivity/InputElementStateStack.cs b/Nodify/Interactivity/InputElementStateStack.cs index 43b96a9a..300ca6ef 100644 --- a/Nodify/Interactivity/InputElementStateStack.cs +++ b/Nodify/Interactivity/InputElementStateStack.cs @@ -18,6 +18,8 @@ public partial class InputElementStateStack : IInputHandler /// protected TElement Element { get; } + public bool RequiresInputCapture => State.RequiresInputCapture; + /// /// Initializes a new instance of the class. /// diff --git a/Nodify/Interactivity/InputProcessor.Shared.cs b/Nodify/Interactivity/InputProcessor.Shared.cs index d9ce10b5..3feda93a 100644 --- a/Nodify/Interactivity/InputProcessor.Shared.cs +++ b/Nodify/Interactivity/InputProcessor.Shared.cs @@ -86,6 +86,17 @@ public static void RegisterHandlerFactory(Func fac public static void RemoveHandlerFactory() => _handlerFactories.RemoveAll(x => x.Key == typeof(THandler)); + /// + /// Replaces the registered factory method with another one of the same type. + /// + /// The type of the input handler to replace. + public static void ReplaceHandlerFactory(Func factory) + where THandler : IInputHandler + { + int index = _handlerFactories.FindIndex(x => x.Key == typeof(THandler)); + _handlerFactories[index] = new KeyValuePair>(typeof(THandler), elem => factory(elem)); + } + /// /// Clears all registered handler factories, effectively removing all shared input handlers. /// diff --git a/Nodify/Interactivity/InputProcessor.cs b/Nodify/Interactivity/InputProcessor.cs index a7e204e6..848eaea1 100644 --- a/Nodify/Interactivity/InputProcessor.cs +++ b/Nodify/Interactivity/InputProcessor.cs @@ -8,13 +8,22 @@ namespace Nodify.Interactivity /// public partial class InputProcessor { - private readonly HashSet _handlers = new HashSet(); + private readonly List _handlers = new List(); /// /// Gets or sets a value indicating whether events that have been handled should be processed. /// public bool ProcessHandledEvents { get; set; } + /// + /// Gets a value indicating whether the processor has ongoing interactions that require input capture to remain active. + /// + /// + /// This property can be used to determine whether it is safe to release mouse capture, especially during toggled interactions.
+ /// Toggled interactions usually involve two steps, and it is important to keep the input capture active until the interaction is completed. + ///
+ public bool RequiresInputCapture { get; private set; } + /// /// Adds an input handler to the processor. /// @@ -27,7 +36,7 @@ public void AddHandler(IInputHandler handler) /// /// The type of the handler to remove. public void RemoveHandlers() where T : IInputHandler - => _handlers.RemoveWhere(x => x.GetType() == typeof(T)); + => _handlers.RemoveAll(x => x.GetType() == typeof(T)); /// /// Clears all registered handlers. @@ -41,23 +50,28 @@ public void Clear() /// The input event arguments to process. public void ProcessEvent(InputEventArgs e) { - if (ProcessHandledEvents) + RequiresInputCapture = false; + + if (!ProcessHandledEvents) { - foreach (var handler in _handlers) + for (int i = 0; i < _handlers.Count; i++) { - handler.HandleEvent(e); + IInputHandler handler = _handlers[i]; + if (!e.Handled) + { + handler.HandleEvent(e); + } + + RequiresInputCapture |= handler.RequiresInputCapture; } } else { - foreach (var handler in _handlers) + for (int i = 0; i < _handlers.Count; i++) { - if (e.Handled) - { - break; - } - + IInputHandler handler = _handlers[i]; handler.HandleEvent(e); + RequiresInputCapture |= handler.RequiresInputCapture; } } } diff --git a/Nodify/Minimap/Minimap.cs b/Nodify/Minimap/Minimap.cs index 20515e78..86fcbc6b 100644 --- a/Nodify/Minimap/Minimap.cs +++ b/Nodify/Minimap/Minimap.cs @@ -160,7 +160,7 @@ protected override void OnMouseUp(MouseButtonEventArgs e) InputProcessor.ProcessEvent(e); // Release the mouse capture if all the mouse buttons are released and there's no interaction in progress - if (IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released && !IsToggledInteractionInProgress()) + if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released) { ReleaseMouseCapture(); } @@ -190,7 +190,7 @@ protected override void OnKeyUp(KeyEventArgs e) InputProcessor.ProcessEvent(e); // Release the mouse capture if all the mouse buttons are released and there's no interaction in progress - if (IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released && !IsToggledInteractionInProgress()) + if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released) { ReleaseMouseCapture(); } @@ -200,14 +200,6 @@ protected override void OnKeyUp(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e) => InputProcessor.ProcessEvent(e); - /// - /// Determines whether any toggled interaction is currently in progress. - /// - protected virtual bool IsToggledInteractionInProgress() - { - return MinimapState.EnableToggledPanningMode && IsPanning; - } - #endregion #region Panning