diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d3a6d02..c0301195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Diagrams (2.1.4) - 2021-08-29 + +### Added + +- `MouseDoubleClick` (500ms interval) event in `Diagram`. +- `GetScreenPoint` in `Diagram` in order to get the screen points from a diagram point (e.g. node position). +- `Title` property in `NodeModel`, used by the default node widget. + +### Fixed + +- `ZoomToFit` not triggering `ZoomChanged` event. +- `SourceNode` and `TargetNode` not being set in `BaseLinkModel` when the ports change. + ## Diagrams (2.1.3) - 2021-07-19 ### Added diff --git a/README.md b/README.md index 3ac8d4a6..dcd2df3c 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,11 @@ Repository: https://github.com/Blazor-Diagrams/Blazor.DatabaseDesigner ![](DBDesigner.png) +### Contributing + +All kinds of contributions are welcome! +If you're interested in helping, please create an issue or comment on an existing one to explain what you will be doing. This is because multiple people can be working on the same problem. + ## Feedback -If you find a bug or you want to see a functionality in this library, feel free to open an issue. All kinds of contributions are welcome! +If you find a bug or you want to see a functionality in this library, feel free to open an issue. diff --git a/ZBD_Title.png b/ZBD_Title.png new file mode 100644 index 00000000..6b145691 Binary files /dev/null and b/ZBD_Title.png differ diff --git a/samples/SharedDemo/Demos/Events.razor.cs b/samples/SharedDemo/Demos/Events.razor.cs index 8aed84be..d83cfa26 100644 --- a/samples/SharedDemo/Demos/Events.razor.cs +++ b/samples/SharedDemo/Demos/Events.razor.cs @@ -73,6 +73,12 @@ private void RegisterEvents() events.Add($"MouseClick, Type={m?.GetType().Name}, ModelId={m?.Id}"); StateHasChanged(); }; + + diagram.MouseDoubleClick += (m, e) => + { + events.Add($"MouseDoubleClick, Type={m?.GetType().Name}, ModelId={m?.Id}"); + StateHasChanged(); + }; } private NodeModel NewNode(double x, double y) diff --git a/src/Blazor.Diagrams.Core/Behaviors/EventsBehavior.cs b/src/Blazor.Diagrams.Core/Behaviors/EventsBehavior.cs index ba6fe5fe..9a1c6fb5 100644 --- a/src/Blazor.Diagrams.Core/Behaviors/EventsBehavior.cs +++ b/src/Blazor.Diagrams.Core/Behaviors/EventsBehavior.cs @@ -1,19 +1,34 @@ using Blazor.Diagrams.Core.Models.Base; using Microsoft.AspNetCore.Components.Web; using System; +using System.Diagnostics; namespace Blazor.Diagrams.Core.Behaviors { public class EventsBehavior : Behavior { + private readonly Stopwatch _mouseClickSw; private bool _captureMouseMove; private int _mouseMovedCount; public EventsBehavior(Diagram diagram) : base(diagram) { + _mouseClickSw = new Stopwatch(); + Diagram.MouseDown += OnMouseDown; Diagram.MouseMove += OnMouseMove; Diagram.MouseUp += OnMouseUp; + Diagram.MouseClick += OnMouseClick; + } + + private void OnMouseClick(Model model, MouseEventArgs e) + { + if (_mouseClickSw.IsRunning && _mouseClickSw.ElapsedMilliseconds <= 500) + { + Diagram.OnMouseDoubleClick(model, e); + } + + _mouseClickSw.Restart(); } private void OnMouseDown(Model model, MouseEventArgs e) diff --git a/src/Blazor.Diagrams.Core/Blazor.Diagrams.Core.csproj b/src/Blazor.Diagrams.Core/Blazor.Diagrams.Core.csproj index b6fb2f98..5b5abb51 100644 --- a/src/Blazor.Diagrams.Core/Blazor.Diagrams.Core.csproj +++ b/src/Blazor.Diagrams.Core/Blazor.Diagrams.Core.csproj @@ -7,10 +7,10 @@ MIT zHaytam A fully customizable and extensible all-purpose diagrams library for Blazor - 2.1.3 - 2.1.3 + 2.1.4 + 2.1.4 https://github.com/zHaytam/Blazor.Diagrams - 2.1.3 + 2.1.4 Z.Blazor.Diagrams.Core blazor diagrams diagramming svg drag Z.Blazor.Diagrams.Core diff --git a/src/Blazor.Diagrams.Core/Diagram.cs b/src/Blazor.Diagrams.Core/Diagram.cs index 73da83b0..a3795097 100644 --- a/src/Blazor.Diagrams.Core/Diagram.cs +++ b/src/Blazor.Diagrams.Core/Diagram.cs @@ -27,6 +27,7 @@ public class Diagram : Model public event Action? KeyDown; public event Action? Wheel; public event Action? MouseClick; + public event Action? MouseDoubleClick; public event Action? TouchStart; public event Action? TouchMove; public event Action? TouchEnd; @@ -294,11 +295,15 @@ public void ZoomToFit(double margin = 10) var xf = Container.Width / width; var yf = Container.Height / height; - Zoom = Math.Min(xf, yf); var nx = Container.Left + Pan.X + minX * Zoom; var ny = Container.Top + Pan.Y + minY * Zoom; + + SuspendRefresh = true; + SetZoom(Math.Min(xf, yf)); UpdatePan(Container.Left - nx, Container.Top - ny); + SuspendRefresh = false; + Refresh(); } public void UpdatePan(double deltaX, double deltaY) @@ -341,6 +346,16 @@ public Point GetRelativePoint(double clientX, double clientY) return new Point(clientX - Container.Left, clientY - Container.Top); } + public Point GetScreenPoint(double clientX, double clientY) + { + if (Container == null) + throw new Exception("Container not available. Make sure you're not using this method before the diagram is fully loaded"); + + return new Point(Zoom * clientX + Container.Left + Pan.X, Zoom * clientY + Container.Top + Pan.Y); + } + + #region Events + internal void OnMouseDown(Model model, MouseEventArgs e) => MouseDown?.Invoke(model, e); internal void OnMouseMove(Model model, MouseEventArgs e) => MouseMove?.Invoke(model, e); @@ -353,10 +368,14 @@ public Point GetRelativePoint(double clientX, double clientY) internal void OnMouseClick(Model model, MouseEventArgs e) => MouseClick?.Invoke(model, e); + internal void OnMouseDoubleClick(Model model, MouseEventArgs e) => MouseDoubleClick?.Invoke(model, e); + internal void OnTouchStart(Model model, TouchEventArgs e) => TouchStart?.Invoke(model, e); internal void OnTouchMove(Model model, TouchEventArgs e) => TouchMove?.Invoke(model, e); internal void OnTouchEnd(Model model, TouchEventArgs e) => TouchEnd?.Invoke(model, e); + + #endregion } } diff --git a/src/Blazor.Diagrams.Core/Models/Base/BaseLinkModel.cs b/src/Blazor.Diagrams.Core/Models/Base/BaseLinkModel.cs index 31a569f8..2a898a51 100644 --- a/src/Blazor.Diagrams.Core/Models/Base/BaseLinkModel.cs +++ b/src/Blazor.Diagrams.Core/Models/Base/BaseLinkModel.cs @@ -43,8 +43,8 @@ public BaseLinkModel(string id, PortModel sourcePort, PortModel? targetPort = nu TargetNode = targetPort?.Parent; } - public NodeModel SourceNode { get; } - public NodeModel? TargetNode { get; } + public NodeModel SourceNode { get; private set; } + public NodeModel? TargetNode { get; private set; } public PortModel? SourcePort { get; private set; } public PortModel? TargetPort { get; private set; } public bool IsAttached => TargetNode != null || TargetPort != null; @@ -67,6 +67,7 @@ public void SetSourcePort(PortModel port) SourcePort?.RemoveLink(this); SourcePort = port; SourcePort.AddLink(this); + SourceNode = SourcePort.Parent; SourcePortChanged?.Invoke(this, old, SourcePort); } @@ -79,6 +80,7 @@ public void SetTargetPort(PortModel? port) TargetPort?.RemoveLink(this); TargetPort = port; TargetPort?.AddLink(this); + TargetNode = TargetPort?.Parent; TargetPortChanged?.Invoke(this, old, TargetPort); } } diff --git a/src/Blazor.Diagrams.Core/Models/NodeModel.cs b/src/Blazor.Diagrams.Core/Models/NodeModel.cs index ed305009..b01d8d0b 100644 --- a/src/Blazor.Diagrams.Core/Models/NodeModel.cs +++ b/src/Blazor.Diagrams.Core/Models/NodeModel.cs @@ -44,6 +44,7 @@ public Size? Size } } public GroupModel? Group { get; internal set; } + public string Title { get; set; } public IReadOnlyList Ports => _ports; public IReadOnlyList Links => _links; diff --git a/src/Blazor.Diagrams/Blazor.Diagrams.csproj b/src/Blazor.Diagrams/Blazor.Diagrams.csproj index 170a3be5..dc2dcd1a 100644 --- a/src/Blazor.Diagrams/Blazor.Diagrams.csproj +++ b/src/Blazor.Diagrams/Blazor.Diagrams.csproj @@ -5,11 +5,11 @@ 3.0 zHaytam MIT - 2.1.3 - 2.1.3 + 2.1.4 + 2.1.4 https://github.com/zHaytam/Blazor.Diagrams A fully customizable and extensible all-purpose diagrams library for Blazor - 2.1.3 + 2.1.4 true blazor diagrams diagramming svg drag Z.Blazor.Diagrams diff --git a/src/Blazor.Diagrams/Components/NodeWidget.razor b/src/Blazor.Diagrams/Components/NodeWidget.razor index b99a0bb9..25c6515b 100644 --- a/src/Blazor.Diagrams/Components/NodeWidget.razor +++ b/src/Blazor.Diagrams/Components/NodeWidget.razor @@ -1,5 +1,5 @@ 
- Node Title + @(Node.Title ?? "Title") @foreach (var port in Node.Ports) { diff --git a/tests/Blazor.Diagrams.Core.Tests/Behaviors/EventsBehaviorTests.cs b/tests/Blazor.Diagrams.Core.Tests/Behaviors/EventsBehaviorTests.cs new file mode 100644 index 00000000..6a87456c --- /dev/null +++ b/tests/Blazor.Diagrams.Core.Tests/Behaviors/EventsBehaviorTests.cs @@ -0,0 +1,94 @@ +using Blazor.Diagrams.Core.Behaviors; +using FluentAssertions; +using Microsoft.AspNetCore.Components.Web; +using System.Threading.Tasks; +using Xunit; + +namespace Blazor.Diagrams.Core.Tests.Behaviors +{ + public class EventsBehaviorTests + { + [Fact] + public void Behavior_ShouldNotTriggerMouseClick_WhenItsRemoved() + { + // Arrange + var diagram = new Diagram(); + diagram.UnregisterBehavior(); + var eventTriggered = false; + + // Act + diagram.MouseClick += (m, e) => eventTriggered = true; + diagram.OnMouseDown(null, new MouseEventArgs()); + diagram.OnMouseUp(null, new MouseEventArgs()); + + // Assert + eventTriggered.Should().BeFalse(); + } + + [Fact] + public void Behavior_ShouldTriggerMouseClick_WhenMouseDownThenUpWithoutMove() + { + // Arrange + var diagram = new Diagram(); + var eventTriggered = false; + + // Act + diagram.MouseClick += (m, e) => eventTriggered = true; + diagram.OnMouseDown(null, new MouseEventArgs()); + diagram.OnMouseUp(null, new MouseEventArgs()); + + // Assert + eventTriggered.Should().BeTrue(); + } + + [Fact] + public void Behavior_ShouldNotTriggerMouseClick_WhenMouseMoves() + { + // Arrange + var diagram = new Diagram(); + var eventTriggered = false; + + // Act + diagram.MouseClick += (m, e) => eventTriggered = true; + diagram.OnMouseDown(null, new MouseEventArgs()); + diagram.OnMouseMove(null, new MouseEventArgs()); + diagram.OnMouseUp(null, new MouseEventArgs()); + + // Assert + eventTriggered.Should().BeFalse(); + } + + [Fact] + public void Behavior_ShouldTriggerMouseDoubleClick_WhenTwoMouseClicksHappenWithinTime() + { + // Arrange + var diagram = new Diagram(); + var eventTriggered = false; + + // Act + diagram.MouseDoubleClick += (m, e) => eventTriggered = true; + diagram.OnMouseClick(null, new MouseEventArgs()); + diagram.OnMouseClick(null, new MouseEventArgs()); + + // Assert + eventTriggered.Should().BeTrue(); + } + + [Fact] + public async Task Behavior_ShouldNotTriggerMouseDoubleClick_WhenTimeExceeds500() + { + // Arrange + var diagram = new Diagram(); + var eventTriggered = false; + + // Act + diagram.MouseDoubleClick += (m, e) => eventTriggered = true; + diagram.OnMouseClick(null, new MouseEventArgs()); + await Task.Delay(520); + diagram.OnMouseClick(null, new MouseEventArgs()); + + // Assert + eventTriggered.Should().BeFalse(); + } + } +} diff --git a/tests/Blazor.Diagrams.Core.Tests/DiagramTests.cs b/tests/Blazor.Diagrams.Core.Tests/DiagramTests.cs new file mode 100644 index 00000000..44079e78 --- /dev/null +++ b/tests/Blazor.Diagrams.Core.Tests/DiagramTests.cs @@ -0,0 +1,95 @@ +using Blazor.Diagrams.Core.Geometry; +using Blazor.Diagrams.Core.Models; +using FluentAssertions; +using Xunit; + +namespace Blazor.Diagrams.Core.Tests +{ + public class DiagramTests + { + [Fact] + public void GetScreenPoint_ShouldReturnCorrectPoint() + { + // Arrange + var diagram = new Diagram(); + + // Act + diagram.SetZoom(1.234); + diagram.UpdatePan(50, 50); + diagram.SetContainer(new Rectangle(30, 65, 1000, 793)); + var pt = diagram.GetScreenPoint(100, 200); + + // Assert + pt.X.Should().Be(203.4); // 2*X + panX + left + pt.Y.Should().Be(361.8); // 2*Y + panY + top + } + + [Fact] + public void ZoomToFit_ShouldUseSelectedNodesIfAny() + { + // Arrange + var diagram = new Diagram(); + diagram.SetContainer(new Rectangle(new Point(0, 0), new Size(1080, 768))); + diagram.Nodes.Add(new NodeModel(new Point(50, 50)) + { + Size = new Size(100, 80) + }); + diagram.SelectModel(diagram.Nodes[0], true); + + // Act + diagram.ZoomToFit(10); + + // Assert + diagram.Zoom.Should().BeApproximately(7.68, 0.001); + diagram.Pan.X.Should().Be(-40); + diagram.Pan.Y.Should().Be(-40); + } + + [Fact] + public void ZoomToFit_ShouldUseNodesWhenNoneSelected() + { + // Arrange + var diagram = new Diagram(); + diagram.SetContainer(new Rectangle(new Point(0, 0), new Size(1080, 768))); + diagram.Nodes.Add(new NodeModel(new Point(50, 50)) + { + Size = new Size(100, 80) + }); + + // Act + diagram.ZoomToFit(10); + + // Assert + diagram.Zoom.Should().BeApproximately(7.68, 0.001); + diagram.Pan.X.Should().Be(-40); + diagram.Pan.Y.Should().Be(-40); + } + + [Fact] + public void ZoomToFit_ShouldTriggerAppropriateEvents() + { + // Arrange + var diagram = new Diagram(); + diagram.SetContainer(new Rectangle(new Point(0, 0), new Size(1080, 768))); + diagram.Nodes.Add(new NodeModel(new Point(50, 50)) + { + Size = new Size(100, 80) + }); + + var refreshes = 0; + var zoomChanges = 0; + var panChanges = 0; + + // Act + diagram.Changed += () => refreshes++; + diagram.ZoomChanged += () => zoomChanges++; + diagram.PanChanged += () => panChanges++; + diagram.ZoomToFit(10); + + // Assert + refreshes.Should().Be(1); + zoomChanges.Should().Be(1); + panChanges.Should().Be(1); + } + } +} diff --git a/tests/Blazor.Diagrams.Core.Tests/Models/Base/BaseLinkModelTests.cs b/tests/Blazor.Diagrams.Core.Tests/Models/Base/BaseLinkModelTests.cs new file mode 100644 index 00000000..eedfe8d7 --- /dev/null +++ b/tests/Blazor.Diagrams.Core.Tests/Models/Base/BaseLinkModelTests.cs @@ -0,0 +1,93 @@ +using Blazor.Diagrams.Core.Models; +using Blazor.Diagrams.Core.Models.Base; +using FluentAssertions; +using Xunit; + +namespace Blazor.Diagrams.Core.Tests.Models.Base +{ + public class BaseLinkModelTests + { + [Fact] + public void SetSourcePort_ShouldChangePropertiesAndTriggerEvent() + { + // Arrange + var link = new TestLink(sourcePort: new PortModel(null), targetPort: null); + var parent = new NodeModel(); + var sp = new PortModel(parent); + var eventsTriggered = 0; + PortModel oldSp = null; + PortModel newSp = null; + BaseLinkModel linkInstance = null; + + // Act + link.SourcePortChanged += (l, o, n) => + { + eventsTriggered++; + linkInstance = l; + oldSp = o; + newSp = n; + }; + + link.SetSourcePort(sp); + + // Assert + eventsTriggered.Should().Be(1); + link.SourcePort.Should().BeSameAs(sp); + oldSp.Should().NotBeNull(); + newSp.Should().BeSameAs(sp); + linkInstance.Should().BeSameAs(link); + link.SourceNode.Should().BeSameAs(parent); + } + + [Fact] + public void SetTargetPort_ShouldChangePropertiesAndTriggerEvent() + { + // Arrange + var link = new TestLink(sourcePort: new PortModel(null), targetPort: null); + var parent = new NodeModel(); + var tp = new PortModel(parent); + var eventsTriggered = 0; + PortModel oldTp = null; + PortModel newTp = null; + BaseLinkModel linkInstance = null; + + // Act + link.TargetPortChanged += (l, o, n) => + { + eventsTriggered++; + linkInstance = l; + oldTp = o; + newTp = n; + }; + + link.SetTargetPort(tp); + + // Assert + eventsTriggered.Should().Be(1); + link.TargetPort.Should().BeSameAs(tp); + oldTp.Should().BeNull(); + newTp.Should().BeSameAs(tp); + linkInstance.Should().BeSameAs(link); + link.TargetNode.Should().BeSameAs(parent); + } + + private class TestLink : BaseLinkModel + { + public TestLink(NodeModel sourceNode, NodeModel targetNode) : base(sourceNode, targetNode) + { + } + + public TestLink(PortModel sourcePort, PortModel targetPort = null) : base(sourcePort, targetPort) + { + } + + public TestLink(string id, NodeModel sourceNode, NodeModel targetNode) : base(id, sourceNode, targetNode) + { + } + + public TestLink(string id, PortModel sourcePort, PortModel targetPort = null) : base(id, sourcePort, targetPort) + { + } + } + } +}