diff --git a/samples/BehaviorsTestApplication/Views/MainView.axaml b/samples/BehaviorsTestApplication/Views/MainView.axaml index c782e78d2..e91c67ce3 100644 --- a/samples/BehaviorsTestApplication/Views/MainView.axaml +++ b/samples/BehaviorsTestApplication/Views/MainView.axaml @@ -109,6 +109,9 @@ + + + diff --git a/samples/BehaviorsTestApplication/Views/Pages/MouseDragBehaviorView.axaml b/samples/BehaviorsTestApplication/Views/Pages/MouseDragBehaviorView.axaml new file mode 100644 index 000000000..18b48cfe4 --- /dev/null +++ b/samples/BehaviorsTestApplication/Views/Pages/MouseDragBehaviorView.axaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/BehaviorsTestApplication/Views/Pages/MouseDragBehaviorView.axaml.cs b/samples/BehaviorsTestApplication/Views/Pages/MouseDragBehaviorView.axaml.cs new file mode 100644 index 000000000..eac8377ae --- /dev/null +++ b/samples/BehaviorsTestApplication/Views/Pages/MouseDragBehaviorView.axaml.cs @@ -0,0 +1,36 @@ +using System.Linq; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Xaml.Interactivity; +using Avalonia.Xaml.Interactions.Draggable; + +namespace BehaviorsTestApplication.Views.Pages; + +public partial class MouseDragBehaviorView : UserControl +{ + public MouseDragBehaviorView() + { + InitializeComponent(); + + var rect1 = this.FindControl("MultiRect1"); + var rect2 = this.FindControl("MultiRect2"); + var rect3 = this.FindControl("MultiRect3"); + + if (rect1 is not null && rect2 is not null && rect3 is not null) + { + var behavior = Interaction.GetBehaviors(rect1) + .OfType() + .FirstOrDefault(); + if (behavior is not null) + { + behavior.TargetControls.Add(rect2); + behavior.TargetControls.Add(rect3); + } + } + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} diff --git a/src/Avalonia.Xaml.Interactions.Draggable/MouseDragElementBehavior.cs b/src/Avalonia.Xaml.Interactions.Draggable/MouseDragElementBehavior.cs new file mode 100644 index 000000000..7650e6f82 --- /dev/null +++ b/src/Avalonia.Xaml.Interactions.Draggable/MouseDragElementBehavior.cs @@ -0,0 +1,137 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Xaml.Interactivity; + +namespace Avalonia.Xaml.Interactions.Draggable; + +/// +/// Enables dragging of a control with the mouse using a . +/// +public class MouseDragElementBehavior : StyledElementBehavior +{ + /// + /// Identifies the avalonia property. + /// + public static readonly StyledProperty ConstrainToParentBoundsProperty = + AvaloniaProperty.Register(nameof(ConstrainToParentBounds)); + + private bool _captured; + private Point _start; + private Control? _parent; + private TranslateTransform? _transform; + + /// + /// Gets or sets whether dragging should be constrained to the bounds of the parent control. + /// + public bool ConstrainToParentBounds + { + get => GetValue(ConstrainToParentBoundsProperty); + set => SetValue(ConstrainToParentBoundsProperty, value); + } + + /// + protected override void OnAttachedToVisualTree() + { + if (AssociatedObject is not null) + { + AssociatedObject.AddHandler(InputElement.PointerPressedEvent, Pressed, RoutingStrategies.Tunnel); + AssociatedObject.AddHandler(InputElement.PointerReleasedEvent, Released, RoutingStrategies.Tunnel); + AssociatedObject.AddHandler(InputElement.PointerMovedEvent, Moved, RoutingStrategies.Tunnel); + AssociatedObject.AddHandler(InputElement.PointerCaptureLostEvent, CaptureLost, RoutingStrategies.Tunnel); + } + } + + /// + protected override void OnDetachedFromVisualTree() + { + if (AssociatedObject is not null) + { + AssociatedObject.RemoveHandler(InputElement.PointerPressedEvent, Pressed); + AssociatedObject.RemoveHandler(InputElement.PointerReleasedEvent, Released); + AssociatedObject.RemoveHandler(InputElement.PointerMovedEvent, Moved); + AssociatedObject.RemoveHandler(InputElement.PointerCaptureLostEvent, CaptureLost); + } + } + + private void Pressed(object? sender, PointerPressedEventArgs e) + { + var properties = e.GetCurrentPoint(AssociatedObject).Properties; + if (properties.IsLeftButtonPressed && AssociatedObject?.Parent is Control parent) + { + _parent = parent; + _start = e.GetPosition(_parent); + + if (AssociatedObject.RenderTransform is TranslateTransform tr) + { + _transform = tr; + } + else + { + _transform = new TranslateTransform(); + AssociatedObject.RenderTransform = _transform; + } + + _captured = true; + } + } + + private void Released(object? sender, PointerReleasedEventArgs e) + { + if (_captured && e.InitialPressMouseButton == MouseButton.Left) + { + EndDrag(); + } + } + + private void CaptureLost(object? sender, PointerCaptureLostEventArgs e) + { + if (_captured) + { + EndDrag(); + } + } + + private void Moved(object? sender, PointerEventArgs e) + { + var properties = e.GetCurrentPoint(AssociatedObject).Properties; + if (!_captured || !properties.IsLeftButtonPressed || _parent is null || _transform is null) + { + return; + } + + var position = e.GetPosition(_parent); + var deltaX = position.X - _start.X; + var deltaY = position.Y - _start.Y; + _start = position; + + var newX = _transform.X + deltaX; + var newY = _transform.Y + deltaY; + + if (ConstrainToParentBounds && AssociatedObject is Control element) + { + var parentBounds = _parent.Bounds; + var elementBounds = element.Bounds; + + var minX = -elementBounds.X; + var minY = -elementBounds.Y; + var maxX = parentBounds.Width - elementBounds.Width - elementBounds.X; + var maxY = parentBounds.Height - elementBounds.Height - elementBounds.Y; + + newX = Math.Min(Math.Max(newX, minX), maxX); + newY = Math.Min(Math.Max(newY, minY), maxY); + } + + _transform.X = newX; + _transform.Y = newY; + } + + private void EndDrag() + { + _captured = false; + _parent = null; + _transform = null; + } +} diff --git a/src/Avalonia.Xaml.Interactions.Draggable/MultiMouseDragElementBehavior.cs b/src/Avalonia.Xaml.Interactions.Draggable/MultiMouseDragElementBehavior.cs new file mode 100644 index 000000000..526d23b22 --- /dev/null +++ b/src/Avalonia.Xaml.Interactions.Draggable/MultiMouseDragElementBehavior.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Metadata; +using Avalonia.Xaml.Interactivity; + +namespace Avalonia.Xaml.Interactions.Draggable; + +/// +/// Enables dragging of multiple controls with the mouse using . +/// +public class MultiMouseDragElementBehavior : StyledElementBehavior +{ + private AvaloniaList? _targetControls; + private bool _captured; + private Point _start; + private readonly Dictionary _transforms = new(); + private Control? _parent; + + /// + /// Identifies the avalonia property. + /// + public static readonly DirectProperty> TargetControlsProperty = + AvaloniaProperty.RegisterDirect>(nameof(TargetControls), b => b.TargetControls); + + /// + /// Identifies the avalonia property. + /// + public static readonly StyledProperty ConstrainToParentBoundsProperty = + AvaloniaProperty.Register(nameof(ConstrainToParentBounds)); + + /// + /// Gets the collection of controls that should be dragged together. This is an avalonia property. + /// + [Content] + public AvaloniaList TargetControls => _targetControls ??= []; + + /// + /// Gets or sets whether dragging should be constrained to the bounds of the parent control. + /// + public bool ConstrainToParentBounds + { + get => GetValue(ConstrainToParentBoundsProperty); + set => SetValue(ConstrainToParentBoundsProperty, value); + } + + /// + protected override void OnAttachedToVisualTree() + { + if (AssociatedObject is not null) + { + AssociatedObject.AddHandler(InputElement.PointerPressedEvent, Pressed, RoutingStrategies.Tunnel); + AssociatedObject.AddHandler(InputElement.PointerReleasedEvent, Released, RoutingStrategies.Tunnel); + AssociatedObject.AddHandler(InputElement.PointerMovedEvent, Moved, RoutingStrategies.Tunnel); + AssociatedObject.AddHandler(InputElement.PointerCaptureLostEvent, CaptureLost, RoutingStrategies.Tunnel); + } + } + + /// + protected override void OnDetachedFromVisualTree() + { + if (AssociatedObject is not null) + { + AssociatedObject.RemoveHandler(InputElement.PointerPressedEvent, Pressed); + AssociatedObject.RemoveHandler(InputElement.PointerReleasedEvent, Released); + AssociatedObject.RemoveHandler(InputElement.PointerMovedEvent, Moved); + AssociatedObject.RemoveHandler(InputElement.PointerCaptureLostEvent, CaptureLost); + } + } + + private void Pressed(object? sender, PointerPressedEventArgs e) + { + var properties = e.GetCurrentPoint(AssociatedObject).Properties; + if (properties.IsLeftButtonPressed && AssociatedObject?.Parent is Control parent) + { + _parent = parent; + _start = e.GetPosition(_parent); + _transforms.Clear(); + + AddTransform(AssociatedObject); + foreach (var control in TargetControls) + { + AddTransform(control); + } + + _captured = true; + } + } + + private void Released(object? sender, PointerReleasedEventArgs e) + { + if (_captured && e.InitialPressMouseButton == MouseButton.Left) + { + EndDrag(); + } + } + + private void CaptureLost(object? sender, PointerCaptureLostEventArgs e) + { + if (_captured) + { + EndDrag(); + } + } + + private void Moved(object? sender, PointerEventArgs e) + { + var properties = e.GetCurrentPoint(AssociatedObject).Properties; + if (!_captured || !properties.IsLeftButtonPressed || _parent is null) + { + return; + } + + var position = e.GetPosition(_parent); + var dx = position.X - _start.X; + var dy = position.Y - _start.Y; + _start = position; + + foreach (var kvp in _transforms) + { + var element = kvp.Key; + var transform = kvp.Value; + + var newX = transform.X + dx; + var newY = transform.Y + dy; + + if (ConstrainToParentBounds && element.Parent is Control p) + { + var parentBounds = p.Bounds; + var bounds = element.Bounds; + + var minX = -bounds.X; + var minY = -bounds.Y; + var maxX = parentBounds.Width - bounds.Width - bounds.X; + var maxY = parentBounds.Height - bounds.Height - bounds.Y; + + newX = Math.Min(Math.Max(newX, minX), maxX); + newY = Math.Min(Math.Max(newY, minY), maxY); + } + + transform.X = newX; + transform.Y = newY; + } + } + + private void AddTransform(Control? element) + { + if (element is null) + { + return; + } + + if (element.RenderTransform is TranslateTransform tr) + { + _transforms[element] = tr; + } + else + { + var t = new TranslateTransform(); + element.RenderTransform = t; + _transforms[element] = t; + } + } + + private void EndDrag() + { + _captured = false; + _parent = null; + _transforms.Clear(); + } +}