Undo/Redo
This commit is contained in:
@@ -25,6 +25,7 @@ type
|
|||||||
private
|
private
|
||||||
type
|
type
|
||||||
TNodeMap = TDictionary<IAstIdentity, TAstViewNode>;
|
TNodeMap = TDictionary<IAstIdentity, TAstViewNode>;
|
||||||
|
TUndoStack = TStack<IAstNode>;
|
||||||
|
|
||||||
TTypePropagator = class(TAstVisitor)
|
TTypePropagator = class(TAstVisitor)
|
||||||
private
|
private
|
||||||
@@ -42,21 +43,38 @@ type
|
|||||||
FNodeMap: TNodeMap;
|
FNodeMap: TNodeMap;
|
||||||
FRootViewNode: TAstViewNode;
|
FRootViewNode: TAstViewNode;
|
||||||
|
|
||||||
|
// State Tracking
|
||||||
|
FCurrentAst: IAstNode;
|
||||||
|
|
||||||
|
// Undo/Redo Stacks
|
||||||
|
FUndoStack: TUndoStack;
|
||||||
|
FRedoStack: TUndoStack;
|
||||||
|
FIsUndoing: Boolean;
|
||||||
|
|
||||||
procedure CollectNodes(Parent: TControl);
|
procedure CollectNodes(Parent: TControl);
|
||||||
procedure ClearVisuals;
|
procedure ClearVisuals;
|
||||||
procedure ApplyErrors(const Log: ICompilerLog);
|
procedure ApplyErrors(const Log: ICompilerLog);
|
||||||
procedure ApplyTypes(const TypedAst: IAstNode);
|
procedure ApplyTypes(const TypedAst: IAstNode);
|
||||||
procedure RootResized(Sender: TObject);
|
procedure RootResized(Sender: TObject);
|
||||||
|
|
||||||
// This signature MUST match TOnDragDropEvent in Myc.Fmx.AstEditor.Workspace
|
|
||||||
procedure HandleNodeDragDrop(ASource, ATarget: TControl; AIndex: Integer);
|
procedure HandleNodeDragDrop(ASource, ATarget: TControl; AIndex: Integer);
|
||||||
|
|
||||||
|
procedure CommitSnapshot;
|
||||||
|
|
||||||
public
|
public
|
||||||
constructor Create(const AEnv: TAstEnvironment; AWorkspace: TWorkspace);
|
constructor Create(const AEnv: TAstEnvironment; AWorkspace: TWorkspace);
|
||||||
destructor Destroy; override;
|
destructor Destroy; override;
|
||||||
|
|
||||||
procedure ValidateAndShow;
|
procedure ValidateAndShow;
|
||||||
procedure SetRoot(const AstNode: IAstNode);
|
procedure SetRoot(const AstNode: IAstNode);
|
||||||
|
|
||||||
|
procedure Undo;
|
||||||
|
procedure Redo;
|
||||||
|
function CanUndo: Boolean;
|
||||||
|
function CanRedo: Boolean;
|
||||||
|
|
||||||
|
// Expose current AST for external use (saving etc.)
|
||||||
|
property CurrentAst: IAstNode read FCurrentAst;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
@@ -72,18 +90,88 @@ begin
|
|||||||
inherited Create;
|
inherited Create;
|
||||||
FEnv := AEnv;
|
FEnv := AEnv;
|
||||||
FWorkspace := AWorkspace;
|
FWorkspace := AWorkspace;
|
||||||
// Connect the event
|
|
||||||
FWorkspace.OnNodeDragDrop := HandleNodeDragDrop;
|
FWorkspace.OnNodeDragDrop := HandleNodeDragDrop;
|
||||||
FNodeMap := TNodeMap.Create;
|
FNodeMap := TNodeMap.Create;
|
||||||
|
|
||||||
|
FUndoStack := TUndoStack.Create;
|
||||||
|
FRedoStack := TUndoStack.Create;
|
||||||
|
FIsUndoing := False;
|
||||||
|
FCurrentAst := nil;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
destructor TAstEditorController.Destroy;
|
destructor TAstEditorController.Destroy;
|
||||||
begin
|
begin
|
||||||
|
// Cleaning up FCurrentAst is done by ARC automatically
|
||||||
SetRoot(nil);
|
SetRoot(nil);
|
||||||
FNodeMap.Free;
|
FNodeMap.Free;
|
||||||
|
FUndoStack.Free;
|
||||||
|
FRedoStack.Free;
|
||||||
inherited;
|
inherited;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TAstEditorController.CommitSnapshot;
|
||||||
|
begin
|
||||||
|
// Optimization: Push the already known AST instead of recreating it from UI.
|
||||||
|
// Since AST nodes are immutable interfaces, storing the reference is safe.
|
||||||
|
if Assigned(FCurrentAst) and (not FIsUndoing) then
|
||||||
|
begin
|
||||||
|
FUndoStack.Push(FCurrentAst);
|
||||||
|
FRedoStack.Clear;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TAstEditorController.Undo;
|
||||||
|
var
|
||||||
|
prevAst: IAstNode;
|
||||||
|
begin
|
||||||
|
if FUndoStack.Count = 0 then
|
||||||
|
exit;
|
||||||
|
|
||||||
|
FIsUndoing := True;
|
||||||
|
try
|
||||||
|
// Capture current state for Redo
|
||||||
|
if Assigned(FCurrentAst) then
|
||||||
|
FRedoStack.Push(FCurrentAst);
|
||||||
|
|
||||||
|
// Restore previous state
|
||||||
|
prevAst := FUndoStack.Pop;
|
||||||
|
SetRoot(prevAst);
|
||||||
|
finally
|
||||||
|
FIsUndoing := False;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TAstEditorController.Redo;
|
||||||
|
var
|
||||||
|
nextAst: IAstNode;
|
||||||
|
begin
|
||||||
|
if FRedoStack.Count = 0 then
|
||||||
|
exit;
|
||||||
|
|
||||||
|
FIsUndoing := True;
|
||||||
|
try
|
||||||
|
// Capture current state for Undo
|
||||||
|
if Assigned(FCurrentAst) then
|
||||||
|
FUndoStack.Push(FCurrentAst);
|
||||||
|
|
||||||
|
// Restore next state
|
||||||
|
nextAst := FRedoStack.Pop;
|
||||||
|
SetRoot(nextAst);
|
||||||
|
finally
|
||||||
|
FIsUndoing := False;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TAstEditorController.CanUndo: Boolean;
|
||||||
|
begin
|
||||||
|
Result := FUndoStack.Count > 0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TAstEditorController.CanRedo: Boolean;
|
||||||
|
begin
|
||||||
|
Result := FRedoStack.Count > 0;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TAstEditorController.HandleNodeDragDrop(ASource, ATarget: TControl; AIndex: Integer);
|
procedure TAstEditorController.HandleNodeDragDrop(ASource, ATarget: TControl; AIndex: Integer);
|
||||||
var
|
var
|
||||||
SourceParent: TControl;
|
SourceParent: TControl;
|
||||||
@@ -92,32 +180,25 @@ var
|
|||||||
SourceIdx, TargetIdx: Integer;
|
SourceIdx, TargetIdx: Integer;
|
||||||
NewAst: IAstNode;
|
NewAst: IAstNode;
|
||||||
begin
|
begin
|
||||||
// Validation
|
|
||||||
if (ASource = nil) or (AIndex < 0) then
|
if (ASource = nil) or (AIndex < 0) then
|
||||||
Exit;
|
exit;
|
||||||
if not (ASource is TAstViewNode) then
|
if not (ASource is TAstViewNode) then
|
||||||
Exit;
|
exit;
|
||||||
|
|
||||||
// Explicit cast to TControl to avoid TFmxObject incompatibility
|
|
||||||
if not (ASource.Parent is TControl) then
|
if not (ASource.Parent is TControl) then
|
||||||
Exit;
|
exit;
|
||||||
SourceParent := TControl(ASource.Parent);
|
SourceParent := TControl(ASource.Parent);
|
||||||
|
|
||||||
// We need to find the ViewNode responsible for this container that implements IReorderable.
|
|
||||||
ContainerOwner := nil;
|
ContainerOwner := nil;
|
||||||
|
|
||||||
// 1. Check if the direct parent is the ViewNode (Direct list handler scenario)
|
// Logic to find the Reorderable container (same as before)
|
||||||
// TNodeListHandler adds items directly to the ViewNode, so SourceParent IS the ViewNode.
|
|
||||||
if (SourceParent is TAstViewNode) then
|
if (SourceParent is TAstViewNode) then
|
||||||
begin
|
begin
|
||||||
ContainerOwner := TAstViewNode(SourceParent);
|
ContainerOwner := TAstViewNode(SourceParent);
|
||||||
// Only keep it if it actually supports reordering
|
|
||||||
if not Supports(ContainerOwner.Handler, IReorderable, Reorderable) then
|
if not Supports(ContainerOwner.Handler, IReorderable, Reorderable) then
|
||||||
ContainerOwner := nil;
|
ContainerOwner := nil;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// 2. Fallback: Check grandparent (Intermediate container scenario)
|
|
||||||
// Used when items are wrapped in an intermediate TAutoFitControl (e.g. by AddExpr).
|
|
||||||
if (ContainerOwner = nil) and (SourceParent.Parent is TAstViewNode) then
|
if (ContainerOwner = nil) and (SourceParent.Parent is TAstViewNode) then
|
||||||
begin
|
begin
|
||||||
ContainerOwner := TAstViewNode(SourceParent.Parent);
|
ContainerOwner := TAstViewNode(SourceParent.Parent);
|
||||||
@@ -125,7 +206,6 @@ begin
|
|||||||
ContainerOwner := nil;
|
ContainerOwner := nil;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Execute Move
|
|
||||||
if (ContainerOwner <> nil) and (Reorderable <> nil) then
|
if (ContainerOwner <> nil) and (Reorderable <> nil) then
|
||||||
begin
|
begin
|
||||||
if Reorderable.CanDrag(ASource) then
|
if Reorderable.CanDrag(ASource) then
|
||||||
@@ -135,17 +215,16 @@ begin
|
|||||||
|
|
||||||
if SourceIdx <> TargetIdx then
|
if SourceIdx <> TargetIdx then
|
||||||
begin
|
begin
|
||||||
// A. Update internal state in the Handler (modifies FChildNodes list)
|
// 1. Commit the CURRENT (clean) AST before visual changes
|
||||||
|
CommitSnapshot;
|
||||||
|
|
||||||
|
// 2. Perform Visual Change (Dirty State)
|
||||||
Reorderable.MoveChild(SourceIdx, TargetIdx);
|
Reorderable.MoveChild(SourceIdx, TargetIdx);
|
||||||
|
|
||||||
// B. Rebuild UI Logic
|
// 3. Generate NEW AST from the modified visual tree
|
||||||
// Since the handler's internal list is now reordered, CreateAst
|
|
||||||
// will produce a new AST with the correct order.
|
|
||||||
NewAst := FRootViewNode.CreateAst;
|
NewAst := FRootViewNode.CreateAst;
|
||||||
|
|
||||||
// C. Full UI Refresh
|
// 4. Update the system with the new AST (Updates FCurrentAst)
|
||||||
// ValidateAndShow only annotates. SetRoot destroys the old visual tree
|
|
||||||
// and builds a new one from the freshly generated AST.
|
|
||||||
SetRoot(NewAst);
|
SetRoot(NewAst);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -156,13 +235,22 @@ procedure TAstEditorController.SetRoot(const AstNode: IAstNode);
|
|||||||
var
|
var
|
||||||
Visualizer: IAstVisualizer;
|
Visualizer: IAstVisualizer;
|
||||||
begin
|
begin
|
||||||
|
// Update the "Truth"
|
||||||
|
FCurrentAst := AstNode;
|
||||||
|
|
||||||
|
if not FIsUndoing then
|
||||||
|
begin
|
||||||
|
FUndoStack.Clear;
|
||||||
|
FRedoStack.Clear;
|
||||||
|
end;
|
||||||
|
|
||||||
FWorkspace.World.DeleteChildren;
|
FWorkspace.World.DeleteChildren;
|
||||||
if Assigned(FRootViewNode) then
|
if Assigned(FRootViewNode) then
|
||||||
FRootViewNode.OnResized := nil;
|
FRootViewNode.OnResized := nil;
|
||||||
FRootViewNode := nil;
|
FRootViewNode := nil;
|
||||||
|
|
||||||
if not Assigned(AstNode) then
|
if not Assigned(AstNode) then
|
||||||
Exit;
|
exit;
|
||||||
|
|
||||||
Visualizer := TAstVisualizer.Create(FWorkspace, FWorkspace.World, 0);
|
Visualizer := TAstVisualizer.Create(FWorkspace, FWorkspace.World, 0);
|
||||||
FRootViewNode := Visualizer.CallAccept(AstNode);
|
FRootViewNode := Visualizer.CallAccept(AstNode);
|
||||||
@@ -178,6 +266,8 @@ begin
|
|||||||
ValidateAndShow;
|
ValidateAndShow;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// ... (Rest: CollectNodes, ClearVisuals, ApplyErrors, ApplyTypes, RootResized, TTypePropagator bleiben gleich)
|
||||||
|
|
||||||
procedure TAstEditorController.CollectNodes(Parent: TControl);
|
procedure TAstEditorController.CollectNodes(Parent: TControl);
|
||||||
var
|
var
|
||||||
i: Integer;
|
i: Integer;
|
||||||
@@ -239,7 +329,7 @@ var
|
|||||||
propagator: TTypePropagator;
|
propagator: TTypePropagator;
|
||||||
begin
|
begin
|
||||||
if not Assigned(TypedAst) then
|
if not Assigned(TypedAst) then
|
||||||
Exit;
|
exit;
|
||||||
|
|
||||||
propagator := TTypePropagator.Create(FNodeMap);
|
propagator := TTypePropagator.Create(FNodeMap);
|
||||||
try
|
try
|
||||||
@@ -259,9 +349,20 @@ var
|
|||||||
logicalAst: IAstNode;
|
logicalAst: IAstNode;
|
||||||
begin
|
begin
|
||||||
if not Assigned(FRootViewNode) then
|
if not Assigned(FRootViewNode) then
|
||||||
Exit;
|
exit;
|
||||||
|
|
||||||
|
// Note: We use FCurrentAst here if we trust it, or recreate for safety.
|
||||||
|
// Since SetRoot sets FCurrentAst, and any UI manipulation (DragDrop) calls SetRoot afterwards,
|
||||||
|
// FCurrentAst should be in sync.
|
||||||
|
// However, strictly speaking, ValidateAndShow operates on the VISUAL tree to find Mapping.
|
||||||
|
// But since we have FCurrentAst, we can use it directly for binding.
|
||||||
|
|
||||||
|
// BUT: ValidateAndShow is responsible for mapping NodeMap.
|
||||||
logicalAst := FRootViewNode.CreateAst;
|
logicalAst := FRootViewNode.CreateAst;
|
||||||
|
// ^ This is still needed to rebuild the Mapping if we haven't tracked ViewNodes perfectly.
|
||||||
|
// Or we rely on FCurrentAst being the source of truth and we assume ViewNodes match.
|
||||||
|
// For now, let's keep CreateAst here to be 100% sure the mapping corresponds to what is on screen.
|
||||||
|
// The performance hit here is acceptable as it's part of the validation cycle, not the drag-start.
|
||||||
|
|
||||||
FNodeMap.Clear;
|
FNodeMap.Clear;
|
||||||
CollectNodes(FWorkspace.World);
|
CollectNodes(FWorkspace.World);
|
||||||
@@ -323,7 +424,7 @@ end;
|
|||||||
procedure TAstEditorController.TTypePropagator.Accept(const Node: IAstNode);
|
procedure TAstEditorController.TTypePropagator.Accept(const Node: IAstNode);
|
||||||
begin
|
begin
|
||||||
if Node = nil then
|
if Node = nil then
|
||||||
Exit;
|
exit;
|
||||||
ApplyType(Node);
|
ApplyType(Node);
|
||||||
Node.Accept(Self);
|
Node.Accept(Self);
|
||||||
end;
|
end;
|
||||||
|
|||||||
Reference in New Issue
Block a user