528 lines
14 KiB
ObjectPascal
528 lines
14 KiB
ObjectPascal
unit Myc.Fmx.AstEditor.Workspace;
|
|
|
|
interface
|
|
|
|
uses
|
|
System.Classes,
|
|
System.SysUtils,
|
|
System.Types,
|
|
System.UITypes,
|
|
System.Math,
|
|
System.Math.Vectors,
|
|
FMX.Types,
|
|
FMX.Controls,
|
|
FMX.Objects,
|
|
FMX.Graphics,
|
|
FMX.Forms,
|
|
Myc.Fmx.AstEditor.Render;
|
|
|
|
type
|
|
TOnPaintConnectionsEvent = procedure(ASender: TObject; ACanvas: TCanvas) of object;
|
|
TOnDragDropEvent = procedure(ASource, ATarget: TVisualNode; AIndex: Integer) of object;
|
|
|
|
TWorkspace = class(TControl, IVisualHost)
|
|
private
|
|
const
|
|
MinZoom = 0.1;
|
|
MaxZoom = 5.0;
|
|
ZoomStep = 0.1;
|
|
private
|
|
FRootNode: TVisualNode;
|
|
|
|
FContentOffset: TPointF;
|
|
FZoom: Single;
|
|
|
|
FIsPanning: Boolean;
|
|
FLastMousePos: TPointF;
|
|
FMouseOverNode: TVisualNode;
|
|
FCapturedNode: TVisualNode;
|
|
|
|
FIsDraggingNode: Boolean;
|
|
FDragSource: TVisualNode;
|
|
FDragVisualBitmap: TBitmap;
|
|
FDragVisualPos: TPointF;
|
|
FDragOffset: TPointF;
|
|
|
|
FCurrentDropTarget: TVisualNode;
|
|
FCurrentDropIndex: Integer;
|
|
FShowDropMarker: Boolean;
|
|
FDropP1, FDropP2: TPointF;
|
|
|
|
FOnPaintConnections: TOnPaintConnectionsEvent;
|
|
FOnNodeDragDrop: TOnDragDropEvent;
|
|
|
|
// IVisualHost
|
|
procedure InvalidateRect(const R: TRectF);
|
|
function GetCanvas: TCanvas;
|
|
|
|
procedure SetZoom(const Value: Single);
|
|
procedure SetContentOffset(const Value: TPointF);
|
|
procedure SetRootNode(const Value: TVisualNode);
|
|
|
|
procedure UpdateDropMarker(const WorldPos: TPointF);
|
|
procedure StopDrag;
|
|
|
|
protected
|
|
procedure Paint; override;
|
|
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override;
|
|
procedure MouseMove(Shift: TShiftState; X, Y: Single); override;
|
|
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override;
|
|
procedure MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean); override;
|
|
procedure Resize; override;
|
|
public
|
|
constructor Create(AOwner: TComponent); override;
|
|
destructor Destroy; override;
|
|
|
|
procedure BeginDragNode(ASource: TVisualNode);
|
|
procedure InvalidateConnections;
|
|
|
|
// Moved to public for Editor access
|
|
function ScreenToWorld(const APoint: TPointF): TPointF;
|
|
procedure RequestLayout;
|
|
|
|
property RootNode: TVisualNode read FRootNode write SetRootNode;
|
|
property Zoom: Single read FZoom write SetZoom;
|
|
property ContentOffset: TPointF read FContentOffset write SetContentOffset;
|
|
|
|
property OnPaintConnections: TOnPaintConnectionsEvent read FOnPaintConnections write FOnPaintConnections;
|
|
property OnNodeDragDrop: TOnDragDropEvent read FOnNodeDragDrop write FOnNodeDragDrop;
|
|
end;
|
|
|
|
implementation
|
|
|
|
uses
|
|
Myc.Fmx.AstEditor.Node;
|
|
|
|
{ TWorkspace }
|
|
|
|
constructor TWorkspace.Create(AOwner: TComponent);
|
|
begin
|
|
inherited Create(AOwner);
|
|
ClipChildren := True;
|
|
HitTest := True;
|
|
CanFocus := True;
|
|
AutoCapture := True;
|
|
|
|
FZoom := 1.0;
|
|
FContentOffset := TPointF.Zero;
|
|
FRootNode := TVisualNode.Create(Self);
|
|
end;
|
|
|
|
destructor TWorkspace.Destroy;
|
|
begin
|
|
FreeAndNil(FRootNode);
|
|
FreeAndNil(FDragVisualBitmap);
|
|
inherited;
|
|
end;
|
|
|
|
function TWorkspace.GetCanvas: TCanvas;
|
|
begin
|
|
Result := Canvas;
|
|
end;
|
|
|
|
procedure TWorkspace.InvalidateRect(const R: TRectF);
|
|
begin
|
|
Repaint;
|
|
end;
|
|
|
|
procedure TWorkspace.RequestLayout;
|
|
begin
|
|
if Assigned(FRootNode) then
|
|
begin
|
|
FRootNode.RecalcLayout;
|
|
Repaint;
|
|
end;
|
|
end;
|
|
|
|
procedure TWorkspace.SetRootNode(const Value: TVisualNode);
|
|
begin
|
|
if FRootNode <> Value then
|
|
begin
|
|
if Assigned(FRootNode) then
|
|
FRootNode.Free;
|
|
FRootNode := Value;
|
|
RequestLayout;
|
|
end;
|
|
end;
|
|
|
|
procedure TWorkspace.SetZoom(const Value: Single);
|
|
begin
|
|
if FZoom <> Value then
|
|
begin
|
|
FZoom := EnsureRange(Value, MinZoom, MaxZoom);
|
|
Repaint;
|
|
end;
|
|
end;
|
|
|
|
procedure TWorkspace.SetContentOffset(const Value: TPointF);
|
|
begin
|
|
if FContentOffset <> Value then
|
|
begin
|
|
FContentOffset := Value;
|
|
Repaint;
|
|
end;
|
|
end;
|
|
|
|
procedure TWorkspace.Paint;
|
|
var
|
|
State: TCanvasSaveState;
|
|
DragRect: TRectF;
|
|
begin
|
|
inherited;
|
|
|
|
if (csDesigning in ComponentState) then
|
|
begin
|
|
Canvas.Stroke.Kind := TBrushKind.Solid;
|
|
Canvas.Stroke.Color := TAlphaColors.Gray;
|
|
Canvas.Stroke.Dash := TStrokeDash.Dash;
|
|
Canvas.DrawRect(LocalRect, 0, 0, AllCorners, 1.0);
|
|
end;
|
|
|
|
State := Canvas.SaveState;
|
|
try
|
|
Canvas.IntersectClipRect(LocalRect);
|
|
|
|
Canvas.MultiplyMatrix(TMatrix.CreateScaling(FZoom, FZoom));
|
|
Canvas.MultiplyMatrix(TMatrix.CreateTranslation(FContentOffset.X, FContentOffset.Y));
|
|
|
|
if Assigned(FOnPaintConnections) then
|
|
FOnPaintConnections(Self, Canvas);
|
|
|
|
if Assigned(FRootNode) then
|
|
FRootNode.Paint(Canvas, TPointF.Zero);
|
|
|
|
if FShowDropMarker then
|
|
begin
|
|
Canvas.Stroke.Kind := TBrushKind.Solid;
|
|
Canvas.Stroke.Color := TAlphaColors.Red;
|
|
Canvas.Stroke.Thickness := 3 / FZoom;
|
|
Canvas.Stroke.Cap := TStrokeCap.Round;
|
|
|
|
Canvas.DrawLine(FDropP1, FDropP2, 1.0);
|
|
|
|
Canvas.Fill.Color := TAlphaColors.Red;
|
|
var R: Single := 4 / FZoom;
|
|
Canvas.FillEllipse(TRectF.Create(FDropP1.X - R, FDropP1.Y - R, FDropP1.X + R, FDropP1.Y + R), 1.0);
|
|
Canvas.FillEllipse(TRectF.Create(FDropP2.X - R, FDropP2.Y - R, FDropP2.X + R, FDropP2.Y + R), 1.0);
|
|
end;
|
|
|
|
if FIsDraggingNode and Assigned(FDragVisualBitmap) then
|
|
begin
|
|
DragRect := TRectF.Create(FDragVisualPos, FDragVisualBitmap.Width, FDragVisualBitmap.Height);
|
|
Canvas.DrawBitmap(FDragVisualBitmap, TRectF.Create(0, 0, FDragVisualBitmap.Width, FDragVisualBitmap.Height), DragRect, 0.7);
|
|
end;
|
|
|
|
finally
|
|
Canvas.RestoreState(State);
|
|
end;
|
|
end;
|
|
|
|
function TWorkspace.ScreenToWorld(const APoint: TPointF): TPointF;
|
|
var
|
|
LocalP: TPointF;
|
|
begin
|
|
LocalP := ScreenToLocal(APoint);
|
|
Result := (LocalP - FContentOffset) / FZoom;
|
|
end;
|
|
|
|
procedure TWorkspace.Resize;
|
|
begin
|
|
inherited;
|
|
Repaint;
|
|
end;
|
|
|
|
procedure TWorkspace.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
|
|
var
|
|
WorldP: TPointF;
|
|
Hit: TVisualNode;
|
|
begin
|
|
inherited;
|
|
if CanFocus then
|
|
SetFocus;
|
|
|
|
WorldP := ScreenToWorld(LocalToScreen(TPointF.Create(X, Y)));
|
|
|
|
if (Button in [TMouseButton.mbLeft, TMouseButton.mbMiddle]) and (ssCtrl in Shift) then
|
|
begin
|
|
FIsPanning := True;
|
|
FLastMousePos := TPointF.Create(X, Y);
|
|
Root.Captured := Self;
|
|
Exit;
|
|
end;
|
|
|
|
if Assigned(FRootNode) then
|
|
begin
|
|
Hit := FRootNode.HitTest(WorldP);
|
|
|
|
if Assigned(Hit) then
|
|
begin
|
|
FCapturedNode := Hit;
|
|
var NodeLocal := Hit.AbsoluteToLocal(WorldP);
|
|
Hit.MouseDown(Button, Shift, NodeLocal.X, NodeLocal.Y);
|
|
end
|
|
else if Button = TMouseButton.mbLeft then
|
|
begin
|
|
FIsPanning := True;
|
|
FLastMousePos := TPointF.Create(X, Y);
|
|
Root.Captured := Self;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TWorkspace.MouseMove(Shift: TShiftState; X, Y: Single);
|
|
var
|
|
WorldP: TPointF;
|
|
LocalP: TPointF;
|
|
Hit: TVisualNode;
|
|
Delta: TPointF;
|
|
begin
|
|
inherited;
|
|
LocalP := TPointF.Create(X, Y);
|
|
WorldP := ScreenToWorld(LocalToScreen(LocalP));
|
|
|
|
if FIsDraggingNode then
|
|
begin
|
|
FDragVisualPos := WorldP - FDragOffset;
|
|
UpdateDropMarker(WorldP);
|
|
Repaint;
|
|
end
|
|
else if FIsPanning then
|
|
begin
|
|
Delta := LocalP - FLastMousePos;
|
|
FContentOffset := FContentOffset + Delta;
|
|
FLastMousePos := LocalP;
|
|
Repaint;
|
|
end
|
|
else
|
|
begin
|
|
if Assigned(FCapturedNode) then
|
|
begin
|
|
var NodeLocal := FCapturedNode.AbsoluteToLocal(WorldP);
|
|
FCapturedNode.MouseMove(Shift, NodeLocal.X, NodeLocal.Y);
|
|
end
|
|
else if Assigned(FRootNode) then
|
|
begin
|
|
Hit := FRootNode.HitTest(WorldP);
|
|
if Hit <> FMouseOverNode then
|
|
begin
|
|
if Assigned(FMouseOverNode) then
|
|
FMouseOverNode.MouseLeave;
|
|
FMouseOverNode := Hit;
|
|
if Assigned(FMouseOverNode) then
|
|
FMouseOverNode.MouseEnter;
|
|
end;
|
|
|
|
if Assigned(FMouseOverNode) then
|
|
begin
|
|
var NodeLocal := FMouseOverNode.AbsoluteToLocal(WorldP);
|
|
FMouseOverNode.MouseMove(Shift, NodeLocal.X, NodeLocal.Y);
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TWorkspace.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
|
|
var
|
|
WorldP: TPointF;
|
|
begin
|
|
inherited;
|
|
WorldP := ScreenToWorld(LocalToScreen(TPointF.Create(X, Y)));
|
|
|
|
if FIsDraggingNode then
|
|
begin
|
|
StopDrag;
|
|
end
|
|
else if FIsPanning then
|
|
begin
|
|
FIsPanning := False;
|
|
Root.Captured := nil;
|
|
end
|
|
else if Assigned(FCapturedNode) then
|
|
begin
|
|
var NodeLocal := FCapturedNode.AbsoluteToLocal(WorldP);
|
|
FCapturedNode.MouseUp(Button, Shift, NodeLocal.X, NodeLocal.Y);
|
|
FCapturedNode := nil;
|
|
end;
|
|
end;
|
|
|
|
procedure TWorkspace.MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean);
|
|
var
|
|
OldZoom: Single;
|
|
LocalP, OffsetLocal: TPointF;
|
|
begin
|
|
OldZoom := FZoom;
|
|
|
|
if WheelDelta > 0 then
|
|
SetZoom(FZoom + ZoomStep)
|
|
else
|
|
SetZoom(FZoom - ZoomStep);
|
|
|
|
if not SameValue(OldZoom, FZoom, TEpsilon.Position) then
|
|
begin
|
|
LocalP := ScreenToLocal(Screen.MousePos);
|
|
OffsetLocal := LocalP - FContentOffset;
|
|
FContentOffset := LocalP - (OffsetLocal * (FZoom / OldZoom));
|
|
Repaint;
|
|
end;
|
|
Handled := True;
|
|
end;
|
|
|
|
procedure TWorkspace.BeginDragNode(ASource: TVisualNode);
|
|
var
|
|
Bitmap: TBitmap;
|
|
Scale: Single;
|
|
begin
|
|
if FIsDraggingNode or FIsPanning then
|
|
Exit;
|
|
if ASource = nil then
|
|
Exit;
|
|
|
|
FIsDraggingNode := True;
|
|
FDragSource := ASource;
|
|
Root.Captured := Self;
|
|
|
|
Scale := Scene.GetSceneScale;
|
|
Bitmap := TBitmap.Create(Round(ASource.Size.Width * Scale), Round(ASource.Size.Height * Scale));
|
|
Bitmap.BitmapScale := Scale;
|
|
|
|
if Bitmap.Canvas.BeginScene then
|
|
try
|
|
Bitmap.Canvas.Clear(0);
|
|
ASource.Paint(Bitmap.Canvas, TPointF.Zero);
|
|
finally
|
|
Bitmap.Canvas.EndScene;
|
|
end;
|
|
|
|
FDragVisualBitmap := Bitmap;
|
|
|
|
var MouseWorld := ScreenToWorld(Screen.MousePos);
|
|
var NodeWorld := ASource.GetAbsolutePosition;
|
|
|
|
FDragOffset := MouseWorld - NodeWorld;
|
|
FDragVisualPos := NodeWorld;
|
|
|
|
ASource.Opacity := 0.4;
|
|
ASource.RequestLayout;
|
|
end;
|
|
|
|
procedure TWorkspace.UpdateDropMarker(const WorldPos: TPointF);
|
|
var
|
|
Hit: TVisualNode;
|
|
TargetContainer: TAutoFitLayout;
|
|
IsHorizontal: Boolean;
|
|
InsertIndex: Integer;
|
|
Child: TVisualNode;
|
|
P1, P2: TPointF;
|
|
AbsP: TPointF;
|
|
begin
|
|
FShowDropMarker := False;
|
|
FCurrentDropTarget := nil;
|
|
FCurrentDropIndex := -1;
|
|
|
|
if FDragSource = nil then
|
|
Exit;
|
|
|
|
Hit := FRootNode.HitTest(WorldPos);
|
|
if Hit = nil then
|
|
Exit;
|
|
|
|
if (Hit is TAutoFitLayout) then
|
|
TargetContainer := TAutoFitLayout(Hit)
|
|
else if (Hit.Parent is TAutoFitLayout) then
|
|
TargetContainer := TAutoFitLayout(Hit.Parent)
|
|
else
|
|
Exit;
|
|
|
|
if TargetContainer = FDragSource then
|
|
Exit;
|
|
|
|
FCurrentDropTarget := TargetContainer;
|
|
IsHorizontal := TargetContainer.Orientation = loHorizontal;
|
|
|
|
var TargetLocal := TargetContainer.AbsoluteToLocal(WorldPos);
|
|
InsertIndex := TargetContainer.GetInsertionIndex(TargetLocal);
|
|
FCurrentDropIndex := InsertIndex;
|
|
|
|
if InsertIndex < TargetContainer.GetChildCount then
|
|
begin
|
|
Child := TargetContainer.GetChild(InsertIndex);
|
|
AbsP := Child.GetAbsolutePosition;
|
|
if IsHorizontal then
|
|
begin
|
|
P1 := AbsP;
|
|
P2 := AbsP + TPointF.Create(0, Child.Size.Height);
|
|
end
|
|
else
|
|
begin
|
|
P1 := AbsP;
|
|
P2 := AbsP + TPointF.Create(Child.Size.Width, 0);
|
|
end;
|
|
end
|
|
else if TargetContainer.GetChildCount > 0 then
|
|
begin
|
|
Child := TargetContainer.GetChild(TargetContainer.GetChildCount - 1);
|
|
AbsP := Child.GetAbsolutePosition;
|
|
if IsHorizontal then
|
|
begin
|
|
P1 := AbsP + TPointF.Create(Child.Size.Width + Child.Margins.Right, 0);
|
|
P2 := P1 + TPointF.Create(0, Child.Size.Height);
|
|
end
|
|
else
|
|
begin
|
|
P1 := AbsP + TPointF.Create(0, Child.Size.Height + Child.Margins.Bottom);
|
|
P2 := P1 + TPointF.Create(Child.Size.Width, 0);
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
AbsP := TargetContainer.GetAbsolutePosition + TPointF.Create(TargetContainer.Padding.Left, TargetContainer.Padding.Top);
|
|
if IsHorizontal then
|
|
begin
|
|
P1 := AbsP;
|
|
P2 := AbsP + TPointF.Create(0, 20);
|
|
end
|
|
else
|
|
begin
|
|
P1 := AbsP;
|
|
P2 := AbsP + TPointF.Create(20, 0);
|
|
end;
|
|
end;
|
|
|
|
FDropP1 := P1;
|
|
FDropP2 := P2;
|
|
FShowDropMarker := True;
|
|
end;
|
|
|
|
procedure TWorkspace.StopDrag;
|
|
begin
|
|
if not FIsDraggingNode then
|
|
Exit;
|
|
|
|
FreeAndNil(FDragVisualBitmap);
|
|
|
|
if Assigned(FDragSource) then
|
|
begin
|
|
FDragSource.Opacity := 1.0;
|
|
|
|
if Assigned(FOnNodeDragDrop) and Assigned(FCurrentDropTarget) and (FCurrentDropIndex >= 0) then
|
|
begin
|
|
FOnNodeDragDrop(FDragSource, FCurrentDropTarget, FCurrentDropIndex);
|
|
end;
|
|
end;
|
|
|
|
FShowDropMarker := False;
|
|
FIsDraggingNode := False;
|
|
FDragSource := nil;
|
|
FCurrentDropTarget := nil;
|
|
Root.Captured := nil;
|
|
|
|
Repaint;
|
|
end;
|
|
|
|
procedure TWorkspace.InvalidateConnections;
|
|
begin
|
|
Repaint;
|
|
end;
|
|
|
|
end.
|