Undo/Redo

This commit is contained in:
Michael Schimmel
2025-12-01 10:14:33 +01:00
parent 438baa3609
commit 5c738c95bc
+126 -25
View File
@@ -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;