diff --git a/CHANGELOG.md b/CHANGELOG.md index 58322241..a10b8e3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,9 @@ > - Breaking Changes: > - Made the setter of NodifyEditor.IsPanning private > - Made SelectionHelper internal -> - Made Minimap sealed > - Renamed HandleRightClickAfterPanningThreshold to MouseActionSuppressionThreshold in NodifyEditor > - Renamed StartCutting to BeginCutting in NodifyEditor +> - Renamed Connector.EnableStickyConnections to ConnectorState.EnabledToggledConnectingMode > - 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 @@ -24,7 +24,7 @@ > - Added Select, BeginSelecting, UpdateSelection, EndSelecting, CancelSelecting and AllowSelectionCancellation to NodifyEditor > - Added IsDragging, BeginDragging, UpdateDragging, EndDragging and CancelDragging to NodifyEditor > - Added AlignSelection and AlignContainers methods to NodifyEditor -> - Added HasCustomContextMenu dependency property to NodifyEditor, ItemContainer and Connector +> - Added HasCustomContextMenu dependency property to NodifyEditor, ItemContainer, Connector and BaseConnection > - Added Select, BeginDragging, UpdateDragging, EndDragging and CancelDragging to ItemContainer > - Added PreserveSelectionOnRightClick configuration field to ItemContainer > - Added BeginConnecting, UpdatePendingConnection, EndConnecting, CancelConnecting and RemoveConnections methods to Connector @@ -35,6 +35,10 @@ > - 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 +> - Added AllowPanningWhileSelecting, AllowPanningWhileCutting and AllowPanningWhilePushingItems to EditorState +> - Added AllowZoomingWhilePanning, AllowZoomingWhileSelecting, AllowZoomingWhileCutting and AllowZoomingWhilePushingItems to EditorState +> - Added EnableToggledSelectingMode, EnableToggledPanningMode, EnableToggledPushingItemsMode and EnableToggledCuttingMode to EditorState +> - Added MinimapState.EnableToggledPanningMode > - Bugfixes: > - Fixed an issue where the ItemContainer was selected by releasing the mouse button on it, even when the mouse was not captured > - Fixed an issue where the ItemContainer could open its context menu even when it was not selected diff --git a/Examples/Nodify.Playground/EditorSettings.cs b/Examples/Nodify.Playground/EditorSettings.cs index 32219d10..7a23e9f1 100644 --- a/Examples/Nodify.Playground/EditorSettings.cs +++ b/Examples/Nodify.Playground/EditorSettings.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using Nodify.Interactivity; +using System.Collections.Generic; using System.Windows; namespace Nodify.Playground @@ -230,6 +231,11 @@ private EditorSettings() val => Instance.AutoPanningTickRate = val, "Auto panning tick rate: ", "How often is the new position calculated in milliseconds. Disable and enable auto panning for this to have effect."), + new ProxySettingViewModel( + () => Instance.AllowMinimapPanningCancellation, + val => Instance.AllowMinimapPanningCancellation = val, + "Allow minimap panning cancellation: ", + "Right click or escape to cancel panning."), new ProxySettingViewModel( () => Instance.AllowCuttingCancellation, val => Instance.AllowCuttingCancellation = val, @@ -310,6 +316,66 @@ private EditorSettings() val => Instance.FitToScreenExtentMargin = val, "Fit to screen extent margin: ", "Adds some margin to the nodes extent when fit to screen"), + new ProxySettingViewModel( + () => Instance.EnableToggledCutting, + val => Instance.EnableToggledCutting = val, + "Enable toggled cutting mode: ", + "The interaction will be completed in two steps using the same gesture to start and end."), + new ProxySettingViewModel( + () => Instance.EnableToggledPushingItems, + val => Instance.EnableToggledPushingItems = val, + "Enable toggled pushing items mode: ", + "The interaction will be completed in two steps using the same gesture to start and end."), + new ProxySettingViewModel( + () => Instance.EnableToggledPanning, + val => Instance.EnableToggledPanning = val, + "Enable toggled panning mode: ", + "The interaction will be completed in two steps using the same gesture to start and end."), + new ProxySettingViewModel( + () => Instance.EnableToggledSelecting, + 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.EnableMinimapToggledPanning, + val => Instance.EnableMinimapToggledPanning = val, + "Enable minimap toggled panning mode: ", + "The interaction will be completed in two steps using the same gesture to start and end."), + new ProxySettingViewModel( + () => Instance.AllowPanningWhileSelecting, + val => Instance.AllowPanningWhileSelecting = val, + "Allow panning while selecting: ", + "Whether panning is allowed while selecting items in the editor."), + new ProxySettingViewModel( + () => Instance.AllowPanningWhileCutting, + val => Instance.AllowPanningWhileCutting = val, + "Allow panning while cutting: ", + "Whether panning is allowed while cutting connections in the editor."), + new ProxySettingViewModel( + () => Instance.AllowPanningWhilePushingItems, + val => Instance.AllowPanningWhilePushingItems = val, + "Allow panning while pushing items: ", + "Whether panning is allowed while pushing items items in the editor."), + new ProxySettingViewModel( + () => Instance.AllowZoomingWhileSelecting, + val => Instance.AllowZoomingWhileSelecting = val, + "Allow zooming while selecting: ", + "Whether zooming is allowed while selecting items in the editor."), + new ProxySettingViewModel( + () => Instance.AllowZoomingWhileCutting, + val => Instance.AllowZoomingWhileCutting = val, + "Allow zooming while cutting: ", + "Whether zooming is allowed while cutting connections in the editor."), + new ProxySettingViewModel( + () => Instance.AllowZoomingWhilePushingItems, + val => Instance.AllowZoomingWhilePushingItems = val, + "Allow zooming while pushing items: ", + "Whether zooming is allowed while pushing items connections in the editor."), + new ProxySettingViewModel( + () => Instance.AllowZoomingWhilePanning, + val => Instance.AllowZoomingWhilePanning = val, + "Allow zooming while panning: ", + "Whether zooming is allowed while panning connections in the editor."), }; EnableCuttingLinePreview = true; @@ -623,6 +689,12 @@ public double AutoPanningTickRate set => NodifyEditor.AutoPanningTickRate = value; } + public bool AllowMinimapPanningCancellation + { + get => Minimap.AllowPanningCancellation; + set => Minimap.AllowPanningCancellation = value; + } + public bool AllowCuttingCancellation { get => NodifyEditor.AllowCuttingCancellation; @@ -721,8 +793,80 @@ public bool EnableDraggingOptimizations public bool EnableStickyConnectors { - get => Connector.EnableStickyConnections; - set => Connector.EnableStickyConnections = value; + get => ConnectorState.EnableToggledConnectingMode; + set => ConnectorState.EnableToggledConnectingMode = value; + } + + public bool EnableToggledPanning + { + get => EditorState.EnableToggledPanningMode; + set => EditorState.EnableToggledPanningMode = value; + } + + public bool EnableToggledCutting + { + get => EditorState.EnableToggledCuttingMode; + set => EditorState.EnableToggledCuttingMode = value; + } + + public bool EnableToggledPushingItems + { + get => EditorState.EnableToggledPushingItemsMode; + set => EditorState.EnableToggledPushingItemsMode = value; + } + + public bool EnableToggledSelecting + { + get => EditorState.EnableToggledSelectingMode; + set => EditorState.EnableToggledSelectingMode = value; + } + + public bool EnableMinimapToggledPanning + { + get => MinimapState.EnableToggledPanningMode; + set => MinimapState.EnableToggledPanningMode = value; + } + + public bool AllowPanningWhileSelecting + { + get => EditorState.AllowPanningWhileSelecting; + set => EditorState.AllowPanningWhileSelecting = value; + } + + public bool AllowPanningWhileCutting + { + get => EditorState.AllowPanningWhileCutting; + set => EditorState.AllowPanningWhileCutting = value; + } + + public bool AllowPanningWhilePushingItems + { + get => EditorState.AllowPanningWhilePushingItems; + set => EditorState.AllowPanningWhilePushingItems = value; + } + + public bool AllowZoomingWhileSelecting + { + get => EditorState.AllowZoomingWhileSelecting; + set => EditorState.AllowZoomingWhileSelecting = value; + } + + public bool AllowZoomingWhileCutting + { + get => EditorState.AllowZoomingWhileCutting; + set => EditorState.AllowZoomingWhileCutting = value; + } + + public bool AllowZoomingWhilePushingItems + { + get => EditorState.AllowZoomingWhilePushingItems; + set => EditorState.AllowZoomingWhilePushingItems = value; + } + + public bool AllowZoomingWhilePanning + { + get => EditorState.AllowZoomingWhilePanning; + set => EditorState.AllowZoomingWhilePanning = value; } #endregion diff --git a/Examples/Nodify.StateMachine/MainWindow.xaml b/Examples/Nodify.StateMachine/MainWindow.xaml index 9b7bd5f0..e04158b1 100644 --- a/Examples/Nodify.StateMachine/MainWindow.xaml +++ b/Examples/Nodify.StateMachine/MainWindow.xaml @@ -74,10 +74,13 @@ Spacing="0" SourceOffsetMode="Edge" TargetOffsetMode="Edge" + OutlineThickness="5" Tag="{Binding}"> diff --git a/Examples/Nodify.StateMachine/MainWindow.xaml.cs b/Examples/Nodify.StateMachine/MainWindow.xaml.cs index 9719de63..66ff5706 100644 --- a/Examples/Nodify.StateMachine/MainWindow.xaml.cs +++ b/Examples/Nodify.StateMachine/MainWindow.xaml.cs @@ -11,7 +11,7 @@ public MainWindow() { InitializeComponent(); - Connector.EnableStickyConnections = true; + ConnectorState.EnableToggledConnectingMode = true; NodifyEditor.EnableCuttingLinePreview = true; EditorGestures.Mappings.Connection.Disconnect.Value = MultiGesture.None; diff --git a/Nodify/Connections/BaseConnection.cs b/Nodify/Connections/BaseConnection.cs index dfd3e3be..785060d6 100644 --- a/Nodify/Connections/BaseConnection.cs +++ b/Nodify/Connections/BaseConnection.cs @@ -144,6 +144,7 @@ public abstract class BaseConnection : Shape public static readonly DependencyProperty IsSelectableProperty = DependencyProperty.RegisterAttached("IsSelectable", typeof(bool), typeof(BaseConnection), new FrameworkPropertyMetadata(BoxValue.False)); public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.RegisterAttached("IsSelected", typeof(bool), typeof(BaseConnection), new FrameworkPropertyMetadata(BoxValue.False, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnIsSelectedChanged)); + public static readonly DependencyProperty HasCustomContextMenuProperty = NodifyEditor.HasCustomContextMenuProperty.AddOwner(typeof(BaseConnection)); private static void OnIsSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { @@ -438,6 +439,21 @@ public FontStretch FontStretch set => SetValue(FontStretchProperty, value); } + /// + /// Gets or sets a value indicating whether the connection uses a custom context menu. + /// + /// When set to true, the connection handles the right-click event for specific interactions. + public bool HasCustomContextMenu + { + get => (bool)GetValue(HasCustomContextMenuProperty); + set => SetValue(HasCustomContextMenuProperty, value); + } + + /// + /// Gets a value indicating whether the connection has a context menu. + /// + public bool HasContextMenu => ContextMenu != null || HasCustomContextMenu; + #endregion #region Routed Events diff --git a/Nodify/Connections/States/ConnectionState.cs b/Nodify/Connections/States/ConnectionState.cs index 6dd3d898..e139335c 100644 --- a/Nodify/Connections/States/ConnectionState.cs +++ b/Nodify/Connections/States/ConnectionState.cs @@ -6,6 +6,7 @@ internal static void RegisterDefaultHandlers() { InputProcessor.Shared.RegisterHandlerFactory(elem => new Disconnect(elem)); InputProcessor.Shared.RegisterHandlerFactory(elem => new Split(elem)); + InputProcessor.Shared.RegisterHandlerFactory(elem => new Default(elem)); } } } diff --git a/Nodify/Connections/States/Default.cs b/Nodify/Connections/States/Default.cs new file mode 100644 index 00000000..de06b2c2 --- /dev/null +++ b/Nodify/Connections/States/Default.cs @@ -0,0 +1,31 @@ +using System.Windows.Input; + +namespace Nodify.Interactivity +{ + public static partial class ConnectionState + { + /// + /// Represents the default input state for a . + /// + public class Default : InputElementState + { + /// + /// Initializes a new instance of the class. + /// + /// The element associated with this state. + public Default(BaseConnection connection) : base(connection) + { + } + + protected override void OnMouseDown(MouseButtonEventArgs e) + { + // Allow context menu to appear + if (e.ChangedButton == MouseButton.Right && Element.HasContextMenu) + { + Element.Focus(); + e.Handled = true; // prevents the editor capturing the mouse + } + } + } + } +} diff --git a/Nodify/Connectors/Connector.cs b/Nodify/Connectors/Connector.cs index 7835ef27..cb4da2a1 100644 --- a/Nodify/Connectors/Connector.cs +++ b/Nodify/Connectors/Connector.cs @@ -105,7 +105,7 @@ public ICommand? DisconnectCommand /// /// Gets or sets a value indicating whether the connector uses a custom context menu. /// - /// When set to true, the connector handles the right-click event for specific operations. + /// When set to true, the connector handles the right-click event for specific interactions. public bool HasCustomContextMenu { get => (bool)GetValue(HasCustomContextMenuProperty); @@ -157,11 +157,6 @@ public bool HasCustomContextMenu /// public static bool AllowPendingConnectionCancellation { get; set; } = true; - /// - /// Gets or sets whether the connection should be completed in two steps. - /// - public static bool EnableStickyConnections { get; set; } - private Point _lastUpdatedContainerPosition; private Point _pendingConnectionEndPosition; private bool _isHooked; @@ -344,8 +339,8 @@ protected override void OnMouseUp(MouseButtonEventArgs e) { InputProcessor.Process(e); - // Release the mouse capture if all the mouse buttons are released and there's no sticky connection pending - if (!IsPendingConnection && IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released) + // 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()) { ReleaseMouseCapture(); } @@ -368,22 +363,23 @@ protected override void OnKeyUp(KeyEventArgs e) { InputProcessor.Process(e); - if (!IsPendingConnection && IsMouseCaptured) + // 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()) { ReleaseMouseCapture(); } } /// - protected override void OnKeyDown(KeyEventArgs e) - { - InputProcessor.Process(e); + protected override void OnKeyDown(KeyEventArgs e) + => InputProcessor.Process(e); - // Release the mouse capture if all the mouse buttons are released and there's no sticky connection pending - if (!IsPendingConnection && IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released) - { - ReleaseMouseCapture(); - } + /// + /// Determines whether any toggled interaction is currently in progress. + /// + protected virtual bool IsToggledInteractionInProgress() + { + return ConnectorState.EnableToggledConnectingMode && IsPendingConnection; } #endregion diff --git a/Nodify/Connectors/States/Connecting.cs b/Nodify/Connectors/States/Connecting.cs index 9e35c0a4..3b22dc8b 100644 --- a/Nodify/Connectors/States/Connecting.cs +++ b/Nodify/Connectors/States/Connecting.cs @@ -13,6 +13,7 @@ public class Connecting : DragState { protected override bool HasContextMenu => Element.HasContextMenu; protected override bool CanCancel => Connector.AllowPendingConnectionCancellation; + protected override bool IsToggle => EnableToggledConnectingMode; /// /// Initializes a new instance of the class. @@ -24,8 +25,6 @@ public Connecting(Connector connector) PositionElement = Element.Editor ?? (IInputElement)Element; } - protected override bool IsToggle => Connector.EnableStickyConnections; - protected override void OnBegin(InputEventArgs e) => Element.BeginConnecting(); diff --git a/Nodify/Connectors/States/ConnectorState.cs b/Nodify/Connectors/States/ConnectorState.cs index d879cd97..9f8b1239 100644 --- a/Nodify/Connectors/States/ConnectorState.cs +++ b/Nodify/Connectors/States/ConnectorState.cs @@ -2,6 +2,11 @@ { public static partial class ConnectorState { + /// + /// Determines whether toggled connecting mode is enabled, allowing the user to start and end the interaction in two steps with the same input gesture. + /// + public static bool EnableToggledConnectingMode { get; set; } + internal static void RegisterDefaultHandlers() { InputProcessor.Shared.RegisterHandlerFactory(elem => new Disconnect(elem)); diff --git a/Nodify/Containers/ItemContainer.cs b/Nodify/Containers/ItemContainer.cs index 16880ec1..34e566da 100644 --- a/Nodify/Containers/ItemContainer.cs +++ b/Nodify/Containers/ItemContainer.cs @@ -150,7 +150,7 @@ public bool IsDraggable /// /// Gets or sets a value indicating whether the container uses a custom context menu. /// - /// When set to true, the container handles the right-click event for specific operations. + /// When set to true, the container handles the right-click event for specific interactions. public bool HasCustomContextMenu { get => (bool)GetValue(HasCustomContextMenuProperty); @@ -375,9 +375,9 @@ protected override void OnMouseDown(MouseButtonEventArgs e) protected override void OnMouseUp(MouseButtonEventArgs e) { InputProcessor.Process(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) + + // 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()) { ReleaseMouseCapture(); } @@ -400,8 +400,8 @@ protected override void OnKeyUp(KeyEventArgs e) { InputProcessor.Process(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) + // 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()) { ReleaseMouseCapture(); } @@ -411,6 +411,11 @@ protected override void OnKeyUp(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e) => InputProcessor.Process(e); + /// + /// Determines whether any toggled interaction is currently in progress. + /// + protected virtual bool IsToggledInteractionInProgress() => false; + #endregion } } diff --git a/Nodify/Containers/States/Default.cs b/Nodify/Containers/States/Default.cs index 6c56dda3..e1fa0adf 100644 --- a/Nodify/Containers/States/Default.cs +++ b/Nodify/Containers/States/Default.cs @@ -68,18 +68,6 @@ protected override void OnMouseDown(MouseButtonEventArgs e) } } - private bool CaptureMouseSafe() - { - // Avoid stealing mouse capture from other elements - if (Mouse.Captured == null || Element.IsMouseCaptured) - { - Element.CaptureMouse(); - return true; - } - - return false; - } - /// protected override void OnMouseMove(MouseEventArgs e) { @@ -119,14 +107,6 @@ protected override void OnMouseUp(MouseButtonEventArgs e) _selectionType = null; } - private static SelectionType GetSelectionTypeForDragging(SelectionType? selectionType) - { - // Always select the container when dragging - return selectionType == SelectionType.Remove - ? SelectionType.Replace - : selectionType.GetValueOrDefault(SelectionType.Replace); - } - private bool IsSelectable(MouseButtonEventArgs e) { if (!Element.IsSelectableLocation(e.GetPosition(Element))) @@ -141,6 +121,26 @@ private bool IsSelectable(MouseButtonEventArgs e) return true; } + + private bool CaptureMouseSafe() + { + // Avoid stealing mouse capture from other elements + if (Mouse.Captured == null || Element.IsMouseCaptured) + { + Element.CaptureMouse(); + return true; + } + + return false; + } + + private static SelectionType GetSelectionTypeForDragging(SelectionType? selectionType) + { + // Always select the container when dragging + return selectionType == SelectionType.Remove + ? SelectionType.Replace + : selectionType.GetValueOrDefault(SelectionType.Replace); + } } } } diff --git a/Nodify/Editor/NodifyEditor.PushingItems.cs b/Nodify/Editor/NodifyEditor.PushingItems.cs index df7f5307..c7bcf8ae 100644 --- a/Nodify/Editor/NodifyEditor.PushingItems.cs +++ b/Nodify/Editor/NodifyEditor.PushingItems.cs @@ -9,6 +9,8 @@ namespace Nodify [StyleTypedProperty(Property = nameof(PushedAreaStyle), StyleTargetType = typeof(Rectangle))] public partial class NodifyEditor { + #region Dependency properties + public static readonly DependencyProperty PushedAreaStyleProperty = DependencyProperty.Register(nameof(PushedAreaStyle), typeof(Style), typeof(NodifyEditor)); protected static readonly DependencyPropertyKey PushedAreaPropertyKey = DependencyProperty.RegisterReadOnly(nameof(PushedArea), typeof(Rect), typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.Rect)); @@ -56,6 +58,8 @@ public Style PushedAreaStyle set => SetValue(PushedAreaStyleProperty, value); } + #endregion + /// /// Gets or sets whether push items cancellation is allowed (see ). /// diff --git a/Nodify/Editor/NodifyEditor.cs b/Nodify/Editor/NodifyEditor.cs index 59b721c7..4f6f8c06 100644 --- a/Nodify/Editor/NodifyEditor.cs +++ b/Nodify/Editor/NodifyEditor.cs @@ -407,7 +407,7 @@ public bool DisableZooming /// /// Gets or sets a value indicating whether the editor uses a custom context menu. /// - /// When set to true, the editor handles the right-click event for specific operations. + /// When set to true, the editor handles the right-click event for specific interactions. public bool HasCustomContextMenu { get => (bool)GetValue(HasCustomContextMenuProperty); @@ -807,8 +807,8 @@ protected override void OnMouseUp(MouseButtonEventArgs e) MouseLocation = e.GetPosition(ItemsHost); InputProcessor.Process(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) + // 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()) { ReleaseMouseCapture(); } @@ -837,8 +837,8 @@ protected override void OnKeyUp(KeyEventArgs e) { InputProcessor.Process(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) + // 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()) { ReleaseMouseCapture(); } @@ -848,6 +848,17 @@ protected override void OnKeyUp(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e) => InputProcessor.Process(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/Editor/States/Cutting.cs b/Nodify/Editor/States/Cutting.cs index 439f9ad3..c112f459 100644 --- a/Nodify/Editor/States/Cutting.cs +++ b/Nodify/Editor/States/Cutting.cs @@ -10,7 +10,9 @@ public static partial class EditorState public class Cutting : DragState { protected override bool HasContextMenu => Element.HasContextMenu; + protected override bool CanBegin => !Element.IsSelecting && !Element.IsPanning && !Element.IsPushingItems; protected override bool CanCancel => NodifyEditor.AllowCuttingCancellation; + protected override bool IsToggle => EnableToggledCuttingMode; /// /// Initializes a new instance of the class. diff --git a/Nodify/Editor/States/EditorState.cs b/Nodify/Editor/States/EditorState.cs index d263dd5f..b2f0bc03 100644 --- a/Nodify/Editor/States/EditorState.cs +++ b/Nodify/Editor/States/EditorState.cs @@ -2,6 +2,61 @@ { public static partial class EditorState { + /// + /// Gets or sets a value indicating whether panning is allowed while selecting items in the editor. + /// + public static bool AllowPanningWhileSelecting { get; set; } = true; + + /// + /// Gets or sets a value indicating whether panning is allowed while cutting connections in the editor. + /// + public static bool AllowPanningWhileCutting { get; set; } + + /// + /// Gets or sets a value indicating whether panning is allowed while pushing items in the editor. + /// + public static bool AllowPanningWhilePushingItems { get; set; } + + /// + /// Gets or sets a value indicating whether zooming is allowed while selecting items in the editor. + /// + public static bool AllowZoomingWhileSelecting { get; set; } = true; + + /// + /// Gets or sets a value indicating whether zooming is allowed while cutting connections in the editor. + /// + public static bool AllowZoomingWhileCutting { get; set; } = true; + + /// + /// Gets or sets a value indicating whether zooming is allowed while pushing items in the editor. + /// + public static bool AllowZoomingWhilePushingItems { get; set; } = true; + + /// + /// Gets or sets a value indicating whether zooming is allowed while panning the editor viewport. + /// + public static bool AllowZoomingWhilePanning { get; set; } = true; + + /// + /// Determines whether toggled selecting mode is enabled, allowing the user to start and end the interaction in two steps with the same input gesture. + /// + public static bool EnableToggledSelectingMode { get; set; } + + /// + /// Determines whether toggled panning mode is enabled, allowing the user to start and end the interaction in two steps with the same input gesture. + /// + public static bool EnableToggledPanningMode { get; set; } + + /// + /// Determines whether toggled cutting mode is enabled, allowing the user to start and end the interaction in two steps with the same input gesture. + /// + public static bool EnableToggledCuttingMode { get; set; } + + /// + /// Determines whether toggled pushing items mode is enabled, allowing the user to start and end the interaction in two steps with the same input gesture. + /// + public static bool EnableToggledPushingItemsMode { get; set; } + internal static void RegisterDefaultHandlers() { InputProcessor.Shared.RegisterHandlerFactory(elem => new Panning(elem)); diff --git a/Nodify/Editor/States/Panning.cs b/Nodify/Editor/States/Panning.cs index 80c17610..98c495c3 100644 --- a/Nodify/Editor/States/Panning.cs +++ b/Nodify/Editor/States/Panning.cs @@ -12,8 +12,10 @@ public static partial class EditorState public class Panning : DragState { protected override bool HasContextMenu => Element.HasContextMenu; - protected override bool CanBegin => !Element.DisablePanning; + protected override bool CanBegin => IsPanningAllowed(); protected override bool CanCancel => NodifyEditor.AllowPanningCancellation; + protected override bool IsToggle => EnableToggledPanningMode; + private Point _prevPosition; @@ -44,6 +46,14 @@ protected override void OnEnd(InputEventArgs e) protected override void OnCancel(InputEventArgs e) => Element.CancelPanning(); + + private bool IsPanningAllowed() + { + return !Element.DisablePanning + && (AllowPanningWhileSelecting || !Element.IsSelecting) + && (AllowPanningWhileCutting || !Element.IsCutting) + && (AllowPanningWhilePushingItems || !Element.IsPushingItems); + } } /// diff --git a/Nodify/Editor/States/PushingItems.cs b/Nodify/Editor/States/PushingItems.cs index 3df87b68..5dc29e49 100644 --- a/Nodify/Editor/States/PushingItems.cs +++ b/Nodify/Editor/States/PushingItems.cs @@ -13,7 +13,9 @@ public static partial class EditorState public class PushingItems : DragState { protected override bool HasContextMenu => Element.HasContextMenu; + protected override bool CanBegin => !Element.IsSelecting && !Element.IsPanning && !Element.IsCutting; protected override bool CanCancel => NodifyEditor.AllowPushItemsCancellation; + protected override bool IsToggle => EnableToggledPushingItemsMode; private Point _prevPosition; diff --git a/Nodify/Editor/States/Selecting.cs b/Nodify/Editor/States/Selecting.cs index dcbbf71d..41400948 100644 --- a/Nodify/Editor/States/Selecting.cs +++ b/Nodify/Editor/States/Selecting.cs @@ -11,8 +11,9 @@ public static partial class EditorState public class Selecting : DragState { protected override bool HasContextMenu => Element.HasContextMenu; - protected override bool CanBegin => Element.CanSelectMultipleItems && !Element.IsPanning; + protected override bool CanBegin => Element.CanSelectMultipleItems && !Element.IsPanning && !Element.IsCutting && !Element.IsPushingItems; protected override bool CanCancel => NodifyEditor.AllowSelectionCancellation; + protected override bool IsToggle => EnableToggledSelectingMode; /// /// Initializes a new instance of the class. diff --git a/Nodify/Editor/States/Zooming.cs b/Nodify/Editor/States/Zooming.cs index 61b23bbb..68fd28bd 100644 --- a/Nodify/Editor/States/Zooming.cs +++ b/Nodify/Editor/States/Zooming.cs @@ -22,13 +22,22 @@ public Zooming(NodifyEditor editor) : base(editor) protected override void OnMouseWheel(MouseWheelEventArgs e) { EditorGestures.NodifyEditorGestures gestures = EditorGestures.Mappings.Editor; - if (gestures.ZoomModifierKey == Keyboard.Modifiers) + if (gestures.ZoomModifierKey == Keyboard.Modifiers && IsZoomingAllowed()) { double zoom = Math.Pow(2.0, e.Delta / 3.0 / Mouse.MouseWheelDeltaForOneLine); Element.ZoomAtPosition(zoom, Element.MouseLocation); e.Handled = true; } } + + private bool IsZoomingAllowed() + { + return !Element.DisableZooming + && (AllowZoomingWhileSelecting || !Element.IsSelecting) + && (AllowZoomingWhileCutting || !Element.IsCutting) + && (AllowZoomingWhilePushingItems || !Element.IsPushingItems) + && (AllowZoomingWhilePanning || !Element.IsPanning); + } } } } diff --git a/Nodify/Interactivity/DragState.cs b/Nodify/Interactivity/DragState.cs index 785da1c5..ec145030 100644 --- a/Nodify/Interactivity/DragState.cs +++ b/Nodify/Interactivity/DragState.cs @@ -4,57 +4,75 @@ namespace Nodify.Interactivity { /// - /// Represents an abstract base class for managing drag operations within a UI element. - /// Provides a framework for handling input gestures such as starting, canceling, and completing drag operations. + /// Represents an abstract base class for managing drag interactions within a UI element. + /// Provides a framework for handling input gestures such as starting, canceling, and completing drag interactions. /// /// The type of that owns the state. public abstract class DragState : InputElementState, IInputHandler where TElement : FrameworkElement { + private enum InteractionState + { + /// + /// Indicates that no drag interaction is active. This is the initial state or the state after a drag interaction has been canceled or completed. + /// + Ready, + + /// + /// Indicates that a drag interaction is currently active and handling input events. This state is entered when a drag begins. + /// + InProgress, + + /// + /// Indicates that a drag interaction is in the process of ending. This state is used to handle toggled interactions (see ). + /// + Ending + } + /// - /// Gets the gesture used to cancel the drag operation, if defined. + /// Gets the gesture used to cancel the drag interaction, if defined. /// protected InputGesture? CancelGesture { get; } /// - /// Gets the gesture used to begin the drag operation. + /// Gets the gesture used to begin the drag interaction. /// protected InputGesture BeginGesture { get; } /// /// Indicates whether the element has a context menu associated with it. /// - /// This property is used to suppress the context menu when a drag operation is performed using the right mouse button. + /// This property is used to suppress the context menu when a drag interaction is performed using the right mouse button. protected virtual bool HasContextMenu => Element.ContextMenu != null; /// - /// Determines if the drag operation can begin (see ). + /// Determines if the drag interaction can begin (see ). /// protected virtual bool CanBegin { get; } = true; /// - /// Determines if the drag operation can be canceled (see ). + /// Determines if the drag interaction can be canceled (see ). /// protected virtual bool CanCancel { get; } = true; /// - /// Indicates if the drag gesture is a toggle, meaning the same gesture can be used to both start and stop the operation. + /// Indicates if the drag gesture is a toggle, meaning the same gesture can be used to both start and stop the interaction. /// protected virtual bool IsToggle { get; } /// - /// Gets or sets the UI element used to calculate the mouse position during the drag operation. + /// Gets or sets the UI element used to calculate the mouse position during the drag interaction. /// protected IInputElement PositionElement { get; set; } - private bool _canReceiveInput; + private InteractionState _interactionState; private Point _initialPosition; /// /// Initializes a new instance of the class with a begin gesture. /// /// The element associated with this state. - /// The gesture used to start the drag operation. + /// The gesture used to start the drag interaction. public DragState(TElement element, InputGesture beginGesture) : base(element) { BeginGesture = beginGesture; @@ -65,8 +83,8 @@ public DragState(TElement element, InputGesture beginGesture) : base(element) /// Initializes a new instance of the class with begin and cancel gestures. /// /// The element associated with this state. - /// The gesture used to start the drag operation. - /// The gesture used to cancel the drag operation. + /// The gesture used to start the drag interaction. + /// The gesture used to cancel the drag interaction. public DragState(TElement element, InputGesture beginGesture, InputGesture cancelGesture) : this(element, beginGesture) { @@ -75,37 +93,107 @@ public DragState(TElement element, InputGesture beginGesture, InputGesture cance void IInputHandler.HandleEvent(InputEventArgs e) { - if (!_canReceiveInput && IsInputEventPressed(e) && CanBegin && BeginGesture.Matches(e.Source, e)) + if (_interactionState == InteractionState.Ready && TryBeginDragging(e)) { - BeginDrag(e); return; } - if (_canReceiveInput && (IsToggle ? IsInputEventPressed(e) : IsInputEventReleased(e)) && BeginGesture.Matches(e.Source, e)) + if (_interactionState == InteractionState.Ending && TryEndDragging(e)) { - EndDrag(e); return; } - if (_canReceiveInput && (e.RoutedEvent == UIElement.LostMouseCaptureEvent || CanCancel && CancelGesture?.Matches(e.Source, e) is true && IsInputEventReleased(e))) + if (_interactionState == InteractionState.InProgress) { - CancelDrag(e); - return; + if (TryEndDragging(e) || TryCancelDragging(e) || TrySuppressContextMenu(e)) + { + return; + } + } + + TryHandleEvent(e); + } + + #region Interaction logic + + private bool TryEndDragging(InputEventArgs e) + { + if (IsToggle && _interactionState == InteractionState.InProgress) + { + return TryDeferToggleInteractionEnd(e); } - if (_canReceiveInput) + return TryEndInteraction(e); + } + + // Delay ending toggle interaction until the gesture is released + private bool TryDeferToggleInteractionEnd(InputEventArgs e) + { + if (IsInputEventPressed(e) && BeginGesture.Matches(e.Source, e)) { + _interactionState = InteractionState.Ending; HandleEvent(e); + return true; } + + return false; } - private void CancelDrag(InputEventArgs e) + // Begin the interaction on gesture press + private bool TryBeginDragging(InputEventArgs e) { - _canReceiveInput = false; - HandleEvent(e); - OnCancel(e); + if (IsInputEventPressed(e) && CanBegin && BeginGesture.Matches(e.Source, e)) + { + BeginDrag(e); + return true; + } - e.Handled = true; + return false; + } + + // End the interaction on gesture release + private bool TryEndInteraction(InputEventArgs e) + { + if (IsInputEventReleased(e) && BeginGesture.Matches(e.Source, e)) + { + EndDrag(e); + return true; + } + + return false; + } + + // Cancel the interaction + private bool TryCancelDragging(InputEventArgs e) + { + if (IsInputCaptureLost(e) || CanCancel && IsInputEventReleased(e) && CancelGesture?.Matches(e.Source, e) is true) + { + CancelDrag(e); + return true; + } + + return false; + } + + // Suppress the context menu if a toggle interaction is in progress + private bool TrySuppressContextMenu(InputEventArgs e) + { + if (IsToggle && e is MouseButtonEventArgs mbe && mbe.ChangedButton == MouseButton.Right) + { + e.Handled = true; + HandleEvent(e); + return true; + } + + return false; + } + + private void TryHandleEvent(InputEventArgs e) + { + if (_interactionState == InteractionState.InProgress || _interactionState == InteractionState.Ending) + { + HandleEvent(e); + } } private void BeginDrag(InputEventArgs e) @@ -113,7 +201,7 @@ private void BeginDrag(InputEventArgs e) // Avoid stealing mouse capture from other elements if (Mouse.Captured == null || Element.IsMouseCaptured) { - _canReceiveInput = true; + _interactionState = InteractionState.InProgress; HandleEvent(e); // Handle the event, otherwise CaptureMouse will send a MouseMove event and the current event will be handled out of order OnBegin(e); @@ -131,11 +219,11 @@ private void BeginDrag(InputEventArgs e) private void EndDrag(InputEventArgs e) { - _canReceiveInput = false; + _interactionState = InteractionState.Ready; 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) + if (HasContextMenu && e is MouseButtonEventArgs mbe && mbe.ChangedButton == MouseButton.Right) { double dragThreshold = NodifyEditor.MouseActionSuppressionThreshold * NodifyEditor.MouseActionSuppressionThreshold; double dragDistance = (mbe.GetPosition(PositionElement) - _initialPosition).LengthSquared; @@ -157,6 +245,22 @@ private void EndDrag(InputEventArgs e) } } + private void CancelDrag(InputEventArgs e) + { + _interactionState = InteractionState.Ready; + HandleEvent(e); + OnCancel(e); + + e.Handled = true; + } + + #endregion + + protected virtual bool IsInputCaptureLost(InputEventArgs e) + { + return e.RoutedEvent == UIElement.LostMouseCaptureEvent; + } + /// /// Determines if the given input event represents the release of an input gesture. /// @@ -196,25 +300,25 @@ protected virtual bool IsInputEventPressed(InputEventArgs e) } /// - /// Called when the drag operation begins. Override to provide custom behavior. + /// Called when the drag interaction begins. Override to provide custom behavior. /// - /// The input event that started the operation. + /// The input event that started the interaction. protected virtual void OnBegin(InputEventArgs e) { } /// - /// Called when the drag operation ends. Override to provide custom behavior. + /// Called when the drag interaction ends. Override to provide custom behavior. /// - /// The input event that ended the operation. + /// The input event that ended the interaction. protected virtual void OnEnd(InputEventArgs e) { } /// - /// Called when the drag operation is canceled. Override to provide custom behavior. + /// Called when the drag interaction is canceled. Override to provide custom behavior. /// - /// The input event that canceled the operation. + /// The input event that canceled the interaction. protected virtual void OnCancel(InputEventArgs e) { } diff --git a/Nodify/Interactivity/InputElementStateStack.DragState.cs b/Nodify/Interactivity/InputElementStateStack.DragState.cs index d93ecec8..3f3718f4 100644 --- a/Nodify/Interactivity/InputElementStateStack.DragState.cs +++ b/Nodify/Interactivity/InputElementStateStack.DragState.cs @@ -6,7 +6,7 @@ namespace Nodify.Interactivity public partial class InputElementStateStack where TElement : FrameworkElement { /// - /// Represents a specialized state for handling drag operations. + /// Represents a specialized state for handling drag interactions. /// public abstract class DragState : InputElementState, IInputHandler { @@ -26,7 +26,7 @@ public abstract class DragState : InputElementState, IInputHandler protected virtual bool HasContextMenu => Element.ContextMenu != null; /// - /// Gets or sets whether the drag operation can be canceled. + /// Gets or sets whether the drag interaction can be canceled. /// protected virtual bool CanCancel { get; } = true; @@ -166,25 +166,25 @@ protected virtual bool IsInputEventReleased(InputEventArgs e) } /// - /// Called when the drag operation begins. Override to provide custom behavior. + /// Called when the drag interaction begins. Override to provide custom behavior. /// - /// The input event that started the operation. + /// The input event that started the interaction. protected virtual void OnBegin(IInputElementState? from) { } /// - /// Called when the drag operation ends. Override to provide custom behavior. + /// Called when the drag interaction ends. Override to provide custom behavior. /// - /// The input event that ended the operation. + /// The input event that ended the interaction. protected virtual void OnEnd(InputEventArgs e) { } /// - /// Called when the drag operation is canceled. Override to provide custom behavior. + /// Called when the drag interaction is canceled. Override to provide custom behavior. /// - /// The input event that canceled the operation. + /// The input event that canceled the interaction. protected virtual void OnCancel(InputEventArgs e) { } diff --git a/Nodify/Interactivity/InputProcessor.Shared.cs b/Nodify/Interactivity/InputProcessor.Shared.cs index 4a739904..949bf9bd 100644 --- a/Nodify/Interactivity/InputProcessor.Shared.cs +++ b/Nodify/Interactivity/InputProcessor.Shared.cs @@ -6,19 +6,17 @@ namespace Nodify.Interactivity { - public sealed partial class InputProcessor + public partial class InputProcessor { /// /// A shared input processor that allows registering and managing global input handlers for a specific type of UI element. /// /// The type of the UI element that the input handlers will be associated with. - public sealed class Shared : IInputHandler + public sealed class Shared : InputProcessor, IInputHandler where TElement : FrameworkElement { private static readonly List>> _handlerFactories = new List>>(); - private readonly List _handlers = new List(); - /// /// Initializes a new instance of the class for the specified UI element. /// @@ -27,7 +25,7 @@ public Shared(TElement element) { foreach (var kvp in _handlerFactories) { - _handlers.Add(kvp.Value(element)); + AddHandler(kvp.Value(element)); } } @@ -53,19 +51,14 @@ static Shared() { MinimapState.RegisterDefaultHandlers(); } - else if(typeof(TElement) == typeof(BaseConnection)) + else if (typeof(TElement) == typeof(BaseConnection)) { ConnectionState.RegisterDefaultHandlers(); } } public void HandleEvent(InputEventArgs e) - { - foreach (var handler in _handlers) - { - handler.HandleEvent(e); - } - } + => Process(e); /// /// Registers a factory method for creating an input handler of the specified type. @@ -115,7 +108,10 @@ public static class InputProcessorExtensions public static void AddSharedHandlers(this InputProcessor inputProcessor, TElement instance) where TElement : FrameworkElement { - inputProcessor.AddHandler(new InputProcessor.Shared(instance)); + inputProcessor.AddHandler(new InputProcessor.Shared(instance) + { + ProcessHandledEvents = inputProcessor.ProcessHandledEvents + }); } } } diff --git a/Nodify/Interactivity/InputProcessor.cs b/Nodify/Interactivity/InputProcessor.cs index 0e7e1647..1812d665 100644 --- a/Nodify/Interactivity/InputProcessor.cs +++ b/Nodify/Interactivity/InputProcessor.cs @@ -6,7 +6,7 @@ namespace Nodify.Interactivity /// /// Processes input events and delegates them to registered handlers. /// - public sealed partial class InputProcessor + public partial class InputProcessor { private readonly HashSet _handlers = new HashSet(); diff --git a/Nodify/Minimap/Minimap.cs b/Nodify/Minimap/Minimap.cs index 45ed4ef1..07ddd618 100644 --- a/Nodify/Minimap/Minimap.cs +++ b/Nodify/Minimap/Minimap.cs @@ -15,7 +15,7 @@ namespace Nodify [StyleTypedProperty(Property = nameof(ViewportStyle), StyleTargetType = typeof(Rectangle))] [StyleTypedProperty(Property = nameof(ItemContainerStyle), StyleTargetType = typeof(MinimapItem))] [TemplatePart(Name = ElementItemsHost, Type = typeof(Panel))] - public sealed class Minimap : ItemsControl + public class Minimap : ItemsControl { private const string ElementItemsHost = "PART_ItemsHost"; @@ -98,12 +98,12 @@ public event ZoomEventHandler Zoom /// /// Gets the panel that holds all the s. /// - private Panel ItemsHost { get; set; } = default!; + protected Panel ItemsHost { get; private set; } = default!; /// /// Whether the user is currently panning the minimap. /// - private bool IsPanning { get; set; } + protected bool IsPanning { get; private set; } /// /// Gets the current mouse location in graph space coordinates (relative to the ). @@ -143,7 +143,7 @@ protected override bool IsItemItsOwnContainerOverride(object item) #region Gesture Handling - private InputProcessor InputProcessor { get; } = new InputProcessor(); + protected InputProcessor InputProcessor { get; } = new InputProcessor(); /// protected override void OnMouseDown(MouseButtonEventArgs e) @@ -158,8 +158,8 @@ protected override void OnMouseUp(MouseButtonEventArgs e) MouseLocation = e.GetPosition(ItemsHost); InputProcessor.Process(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) + // 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()) { ReleaseMouseCapture(); } @@ -188,8 +188,8 @@ protected override void OnKeyUp(KeyEventArgs e) { InputProcessor.Process(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) + // 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()) { ReleaseMouseCapture(); } @@ -199,6 +199,14 @@ protected override void OnKeyUp(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e) => InputProcessor.Process(e); + /// + /// Determines whether any toggled interaction is currently in progress. + /// + protected virtual bool IsToggledInteractionInProgress() + { + return MinimapState.EnableToggledPanningMode && IsPanning; + } + #endregion #region Panning @@ -295,7 +303,7 @@ public void ZoomAtPosition(double zoom, Point location) RaiseEvent(args); } - private void SetViewportLocation(Point location) + protected void SetViewportLocation(Point location) { var position = location - new Vector(ViewportSize.Width / 2, ViewportSize.Height / 2) + (Vector)Extent.Location; diff --git a/Nodify/Minimap/States/MinimapState.cs b/Nodify/Minimap/States/MinimapState.cs index cab112f0..5af3a270 100644 --- a/Nodify/Minimap/States/MinimapState.cs +++ b/Nodify/Minimap/States/MinimapState.cs @@ -2,6 +2,11 @@ { public static partial class MinimapState { + /// + /// Determines whether toggled panning mode is enabled, allowing the user to start and end the interaction in two steps with the same input gesture. + /// + public static bool EnableToggledPanningMode { get; set; } + internal static void RegisterDefaultHandlers() { InputProcessor.Shared.RegisterHandlerFactory(elem => new Panning(elem)); diff --git a/Nodify/Minimap/States/Panning.cs b/Nodify/Minimap/States/Panning.cs index 26f449bf..4d506a23 100644 --- a/Nodify/Minimap/States/Panning.cs +++ b/Nodify/Minimap/States/Panning.cs @@ -9,8 +9,9 @@ public static partial class MinimapState /// public class Panning : DragState { - protected override bool CanCancel => Minimap.AllowPanningCancellation; protected override bool CanBegin => !Element.IsReadOnly; + protected override bool CanCancel => Minimap.AllowPanningCancellation; + protected override bool IsToggle => EnableToggledPanningMode; /// /// Initializes a new instance of the class.