Skip to content

Commit

Permalink
Merge pull request #176 from miroiu/feature/146-minimap
Browse files Browse the repository at this point in the history
Added panning methods to Minimap
  • Loading branch information
miroiu authored Dec 13, 2024
2 parents c8da71c + 804084a commit f1ae858
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 58 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
> - 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 PushItems to UpdatePushedArea and StartPushingItems to BeginPushingItems in NodifyEditor
Expand All @@ -17,7 +18,8 @@
> - Moved AllowDraggingCancellation from ItemContainer to NodifyEditor
> - Moved EditorGestures under the Nodify.Interactivity namespace
> - Features:
> - Added BeginPanning, UpdatePanning, EndPanning, CancelPanning and AllowPanningCancellation to NodifyEditor
> - Added BeginPanning, UpdatePanning, EndPanning, CancelPanning and AllowPanningCancellation to NodifyEditor and Minimap
> - Added MouseLocation, ZoomAtPosition and GetLocationInsideMinimap to Minimap
> - Added UpdateCuttingLine to NodifyEditor
> - Added Select, BeginSelecting, UpdateSelection, EndSelecting, CancelSelecting and AllowSelectionCancellation to NodifyEditor
> - Added IsDragging, BeginDragging, UpdateDragging, EndDragging and CancelDragging to NodifyEditor
Expand All @@ -27,7 +29,7 @@
> - Added PreserveSelectionOnRightClick configuration field to ItemContainer
> - Added BeginConnecting, UpdatePendingConnection, EndConnecting, CancelConnecting and RemoveConnections methods to Connector
> - Added a custom MouseGesture with support for key combinations
> - Added InputProcessor to NodifyEditor, ItemContainer and Connector, enabling the extension of controls with custom states
> - Added InputProcessor to NodifyEditor, ItemContainer, Connector 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 InputProcessor.Shared to enable the addition of global input handlers
Expand Down
18 changes: 9 additions & 9 deletions Nodify/Editor/NodifyEditor.Panning.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ public void UpdatePanning(Vector amount)
ViewportLocation -= amount;
}

/// <summary>
/// Ends the current panning operation, retaining the current <see cref="ViewportLocation"/>.
/// </summary>
/// <remarks>This method has no effect if there's no panning operation in progress.</remarks>
public void EndPanning()
{
IsPanning = false;
}

/// <summary>
/// Cancels the current panning operation and reverts the viewport to its initial location if <see cref="AllowPanningCancellation"/> is true.
/// Otherwise, it ends the panning operation by calling <see cref="EndPanning"/>.
Expand All @@ -144,15 +153,6 @@ public void CancelPanning()
}
}

/// <summary>
/// Ends the current panning operation, retaining the current <see cref="ViewportLocation"/>.
/// </summary>
/// <remarks>This method has no effect if there's no panning operation in progress.</remarks>
public void EndPanning()
{
IsPanning = false;
}

#region Auto panning

private readonly MouseEventArgs _autoPanningEventArgs = new MouseEventArgs(Mouse.PrimaryDevice, 0, Stylus.CurrentStylusDevice)
Expand Down
6 changes: 6 additions & 0 deletions Nodify/Interactivity/Gestures/EditorGestures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -314,12 +314,17 @@ public class MinimapGestures
public MinimapGestures()
{
DragViewport = new MouseGesture(MouseAction.LeftClick);
CancelAction = new AnyGesture(new MouseGesture(MouseAction.RightClick), new KeyGesture(Key.Escape));
ZoomModifierKey = ModifierKeys.None;
}

/// <summary>Gesture to move the viewport inside the <see cref="Minimap" />.</summary>
public InputGestureRef DragViewport { get; }

/// <summary>Gesture to cancel the panning operation.</summary>
/// <remarks>Defaults to <see cref="MouseAction.RightClick"/> or <see cref="Key.Escape"/>.</remarks>
public InputGestureRef CancelAction { get; }

/// <summary>The key modifier required to start zooming by mouse wheel.</summary>
/// <remarks>Defaults to <see cref="ModifierKeys.None"/>.</remarks>
public ModifierKeys ZoomModifierKey { get; set; }
Expand All @@ -329,6 +334,7 @@ public MinimapGestures()
public void Apply(MinimapGestures gestures)
{
DragViewport.Value = gestures.DragViewport.Value;
CancelAction.Value = gestures.CancelAction.Value;
ZoomModifierKey = gestures.ZoomModifierKey;
}
}
Expand Down
4 changes: 4 additions & 0 deletions Nodify/Interactivity/InputProcessor.Shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ static Shared()
{
ConnectorState.RegisterDefaultHandlers();
}
else if (typeof(TElement) == typeof(Minimap))
{
MinimapState.RegisterDefaultHandlers();
}
}

public void HandleEvent(InputEventArgs e)
Expand Down
202 changes: 157 additions & 45 deletions Nodify/Minimap/Minimap.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Nodify.Interactivity;
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
Expand All @@ -14,9 +15,9 @@ namespace Nodify
[StyleTypedProperty(Property = nameof(ViewportStyle), StyleTargetType = typeof(Rectangle))]
[StyleTypedProperty(Property = nameof(ItemContainerStyle), StyleTargetType = typeof(MinimapItem))]
[TemplatePart(Name = ElementItemsHost, Type = typeof(Panel))]
public class Minimap : ItemsControl
public sealed class Minimap : ItemsControl
{
protected const string ElementItemsHost = "PART_ItemsHost";
private const string ElementItemsHost = "PART_ItemsHost";

public static readonly DependencyProperty ViewportLocationProperty = NodifyEditor.ViewportLocationProperty.AddOwner(typeof(Minimap), new FrameworkPropertyMetadata(BoxValue.Point, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty ViewportSizeProperty = NodifyEditor.ViewportSizeProperty.AddOwner(typeof(Minimap));
Expand Down Expand Up @@ -97,19 +98,36 @@ public event ZoomEventHandler Zoom
/// <summary>
/// Gets the panel that holds all the <see cref="MinimapItem"/>s.
/// </summary>
protected internal Panel ItemsHost { get; private set; } = default!;
private Panel ItemsHost { get; set; } = default!;

/// <summary>
/// Whether the user is currently dragging the minimap.
/// Whether the user is currently panning the minimap.
/// </summary>
protected bool IsDragging { get; private set; }
private bool IsPanning { get; set; }

/// <summary>
/// Gets the current mouse location in graph space coordinates (relative to the <see cref="ItemsHost" />).
/// </summary>
public Point MouseLocation { get; private set; }

/// <summary>
/// Gets or sets whether panning cancellation is allowed (see <see cref="EditorGestures.MinimapGestures.CancelAction"/>).
/// </summary>
public static bool AllowPanningCancellation { get; set; } = true;

private Point _initialViewportLocation;

static Minimap()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Minimap), new FrameworkPropertyMetadata(typeof(Minimap)));
ClipToBoundsProperty.OverrideMetadata(typeof(Minimap), new FrameworkPropertyMetadata(BoxValue.True));
}

public Minimap()
{
InputProcessor.AddSharedHandlers(this);
}

public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Expand All @@ -123,72 +141,158 @@ protected override DependencyObject GetContainerForItemOverride()
protected override bool IsItemItsOwnContainerOverride(object item)
=> item is MinimapItem;

protected override void OnLostMouseCapture(MouseEventArgs e)
=> IsDragging = false;
#region Gesture Handling

private InputProcessor InputProcessor { get; } = new InputProcessor();

/// <inheritdoc />
protected override void OnMouseDown(MouseButtonEventArgs e)
{
var gestures = EditorGestures.Mappings.Minimap;
if (!IsReadOnly && gestures.DragViewport.Matches(this, e))
MouseLocation = e.GetPosition(ItemsHost);
InputProcessor.Process(e);
}

/// <inheritdoc />
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)
{
IsDragging = true;
ReleaseMouseCapture();
}
}

SetViewportLocation(e.GetPosition(ItemsHost));
/// <inheritdoc />
protected override void OnMouseMove(MouseEventArgs e)
{
MouseLocation = e.GetPosition(ItemsHost);
InputProcessor.Process(e);
}

e.Handled = true;
/// <inheritdoc />
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
MouseLocation = e.GetPosition(ItemsHost);
InputProcessor.Process(e);
}

if (Mouse.Captured == null || IsMouseCaptured)
{
Focus();
CaptureMouse();
}
/// <inheritdoc />
protected override void OnLostMouseCapture(MouseEventArgs e)
=> InputProcessor.Process(e);

/// <inheritdoc />
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)
{
ReleaseMouseCapture();
}
}

protected override void OnMouseMove(MouseEventArgs e)
/// <inheritdoc />
protected override void OnKeyDown(KeyEventArgs e)
=> InputProcessor.Process(e);

#endregion

#region Panning

/// <summary>
/// Starts the panning operation from the current <see cref="MouseLocation" />.
/// </summary>
/// <remarks>This method has no effect if a panning operation is already in progress.</remarks>
public void BeginPanning()
=> BeginPanning(MouseLocation);

/// <summary>
/// Starts the panning operation from the specified location. Call <see cref="EndPanning"/> to end the panning operation.
/// </summary>
/// <remarks>This method has no effect if a panning operation is already in progress.</remarks>
/// <param name="location">The initial location where panning starts, in graph space coordinates.</param>
public void BeginPanning(Point location)
{
if (IsDragging)
if (IsPanning)
{
SetViewportLocation(e.GetPosition(ItemsHost));
return;
}

IsPanning = true;
_initialViewportLocation = location;
SetViewportLocation(location);
}

protected override void OnMouseUp(MouseButtonEventArgs e)
/// <summary>
/// Sets the viewport location to the specified location.
/// </summary>
/// <param name="location">The location to pan the viewport to.</param>
public void UpdatePanning(Point location)
{
var gestures = EditorGestures.Mappings.Minimap;
if (IsDragging && gestures.DragViewport.Matches(this, e))
Debug.Assert(IsPanning);
SetViewportLocation(location);
}

/// <summary>
/// Ends the current panning operation, retaining the current <see cref="ViewportLocation"/>.
/// </summary>
/// <remarks>This method has no effect if there's no panning operation in progress.</remarks>
public void EndPanning()
{
if (!IsPanning)
{
IsDragging = false;
e.Handled = true;
return;
}

if (IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released)
_initialViewportLocation = MouseLocation;
IsPanning = false;
}

/// <summary>
/// Cancels the current panning operation and reverts the viewport to its initial location if <see cref="AllowPanningCancellation"/> is true.
/// Otherwise, it ends the panning operation by calling <see cref="EndPanning"/>.
/// </summary>
/// <remarks>This method has no effect if there's no panning operation in progress.</remarks>
public void CancelPanning()
{
if (!AllowPanningCancellation)
{
ReleaseMouseCapture();
EndPanning();
return;
}

if (IsPanning)
{
SetViewportLocation(_initialViewportLocation);
IsPanning = false;
}
}

protected override void OnMouseWheel(MouseWheelEventArgs e)
#endregion

/// <summary>
/// Zoom at the specified location in graph space coordinates.
/// </summary>
/// <param name="zoom">The zoom factor.</param>
/// <param name="location">The location to focus when zooming.</param>
public void ZoomAtPosition(double zoom, Point location)
{
if (!IsReadOnly && !e.Handled && EditorGestures.Mappings.Minimap.ZoomModifierKey == Keyboard.Modifiers)
if (!ResizeToViewport)
{
if (!ResizeToViewport)
{
SetViewportLocation(e.GetPosition(ItemsHost));
}

double zoom = Math.Pow(2.0, e.Delta / 3.0 / Mouse.MouseWheelDeltaForOneLine);
var location = ViewportLocation + (Vector)ViewportSize / 2;

var args = new ZoomEventArgs(zoom, location)
{
RoutedEvent = ZoomEvent,
Source = this
};
RaiseEvent(args);

e.Handled = true;
SetViewportLocation(location);
}

var viewportLocation = ViewportLocation + (Vector)ViewportSize / 2;
var args = new ZoomEventArgs(zoom, viewportLocation)
{
RoutedEvent = ZoomEvent,
Source = this
};
RaiseEvent(args);
}

private void SetViewportLocation(Point location)
Expand All @@ -206,5 +310,13 @@ private void SetViewportLocation(Point location)

ViewportLocation = position;
}

/// <summary>
/// Translates the event location to graph space coordinates (relative to the <see cref="ItemsHost" />).
/// </summary>
/// <param name="args">The mouse event.</param>
/// <returns>A location inside the minimap</returns>
public Point GetLocationInsideMinimap(MouseEventArgs args)
=> args.GetPosition(ItemsHost);
}
}
2 changes: 1 addition & 1 deletion Nodify/Minimap/MinimapPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Nodify
{
internal class MinimapPanel : Panel
internal sealed class MinimapPanel : Panel
{
public static readonly DependencyProperty ViewportLocationProperty = NodifyEditor.ViewportLocationProperty.AddOwner(typeof(MinimapPanel), new FrameworkPropertyMetadata(BoxValue.Point, FrameworkPropertyMetadataOptions.AffectsMeasure));
public static readonly DependencyProperty ViewportSizeProperty = NodifyEditor.ViewportSizeProperty.AddOwner(typeof(MinimapPanel), new FrameworkPropertyMetadata(BoxValue.Size, FrameworkPropertyMetadataOptions.AffectsMeasure));
Expand Down
11 changes: 11 additions & 0 deletions Nodify/Minimap/States/MinimapState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Nodify.Interactivity
{
public static partial class MinimapState
{
internal static void RegisterDefaultHandlers()
{
InputProcessor.Shared<Minimap>.RegisterHandlerFactory(elem => new Panning(elem));
InputProcessor.Shared<Minimap>.RegisterHandlerFactory(elem => new Zooming(elem));
}
}
}
Loading

0 comments on commit f1ae858

Please sign in to comment.