diff --git a/Src/AST/Myc.Fmx.AstEditor.Controller.pas b/Src/AST/Myc.Fmx.AstEditor.Controller.pas index c06b429..7a9a4db 100644 --- a/Src/AST/Myc.Fmx.AstEditor.Controller.pas +++ b/Src/AST/Myc.Fmx.AstEditor.Controller.pas @@ -25,6 +25,7 @@ type private type TNodeMap = TDictionary; + TUndoStack = TStack; TTypePropagator = class(TAstVisitor) private @@ -42,21 +43,38 @@ type FNodeMap: TNodeMap; FRootViewNode: TAstViewNode; + // State Tracking + FCurrentAst: IAstNode; + + // Undo/Redo Stacks + FUndoStack: TUndoStack; + FRedoStack: TUndoStack; + FIsUndoing: Boolean; + procedure CollectNodes(Parent: TControl); procedure ClearVisuals; procedure ApplyErrors(const Log: ICompilerLog); procedure ApplyTypes(const TypedAst: IAstNode); procedure RootResized(Sender: TObject); - // This signature MUST match TOnDragDropEvent in Myc.Fmx.AstEditor.Workspace procedure HandleNodeDragDrop(ASource, ATarget: TControl; AIndex: Integer); + procedure CommitSnapshot; + public constructor Create(const AEnv: TAstEnvironment; AWorkspace: TWorkspace); destructor Destroy; override; procedure ValidateAndShow; 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; implementation @@ -72,18 +90,88 @@ begin inherited Create; FEnv := AEnv; FWorkspace := AWorkspace; - // Connect the event FWorkspace.OnNodeDragDrop := HandleNodeDragDrop; FNodeMap := TNodeMap.Create; + + FUndoStack := TUndoStack.Create; + FRedoStack := TUndoStack.Create; + FIsUndoing := False; + FCurrentAst := nil; end; destructor TAstEditorController.Destroy; begin + // Cleaning up FCurrentAst is done by ARC automatically SetRoot(nil); FNodeMap.Free; + FUndoStack.Free; + FRedoStack.Free; inherited; 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); var SourceParent: TControl; @@ -92,32 +180,25 @@ var SourceIdx, TargetIdx: Integer; NewAst: IAstNode; begin - // Validation if (ASource = nil) or (AIndex < 0) then - Exit; + exit; if not (ASource is TAstViewNode) then - Exit; + exit; - // Explicit cast to TControl to avoid TFmxObject incompatibility if not (ASource.Parent is TControl) then - Exit; + exit; SourceParent := TControl(ASource.Parent); - // We need to find the ViewNode responsible for this container that implements IReorderable. ContainerOwner := nil; - // 1. Check if the direct parent is the ViewNode (Direct list handler scenario) - // TNodeListHandler adds items directly to the ViewNode, so SourceParent IS the ViewNode. + // Logic to find the Reorderable container (same as before) if (SourceParent is TAstViewNode) then begin ContainerOwner := TAstViewNode(SourceParent); - // Only keep it if it actually supports reordering if not Supports(ContainerOwner.Handler, IReorderable, Reorderable) then ContainerOwner := nil; 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 begin ContainerOwner := TAstViewNode(SourceParent.Parent); @@ -125,7 +206,6 @@ begin ContainerOwner := nil; end; - // Execute Move if (ContainerOwner <> nil) and (Reorderable <> nil) then begin if Reorderable.CanDrag(ASource) then @@ -135,17 +215,16 @@ begin if SourceIdx <> TargetIdx then 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); - // B. Rebuild UI Logic - // Since the handler's internal list is now reordered, CreateAst - // will produce a new AST with the correct order. + // 3. Generate NEW AST from the modified visual tree NewAst := FRootViewNode.CreateAst; - // C. Full UI Refresh - // ValidateAndShow only annotates. SetRoot destroys the old visual tree - // and builds a new one from the freshly generated AST. + // 4. Update the system with the new AST (Updates FCurrentAst) SetRoot(NewAst); end; end; @@ -156,13 +235,22 @@ procedure TAstEditorController.SetRoot(const AstNode: IAstNode); var Visualizer: IAstVisualizer; begin + // Update the "Truth" + FCurrentAst := AstNode; + + if not FIsUndoing then + begin + FUndoStack.Clear; + FRedoStack.Clear; + end; + FWorkspace.World.DeleteChildren; if Assigned(FRootViewNode) then FRootViewNode.OnResized := nil; FRootViewNode := nil; if not Assigned(AstNode) then - Exit; + exit; Visualizer := TAstVisualizer.Create(FWorkspace, FWorkspace.World, 0); FRootViewNode := Visualizer.CallAccept(AstNode); @@ -178,6 +266,8 @@ begin ValidateAndShow; end; +// ... (Rest: CollectNodes, ClearVisuals, ApplyErrors, ApplyTypes, RootResized, TTypePropagator bleiben gleich) + procedure TAstEditorController.CollectNodes(Parent: TControl); var i: Integer; @@ -239,7 +329,7 @@ var propagator: TTypePropagator; begin if not Assigned(TypedAst) then - Exit; + exit; propagator := TTypePropagator.Create(FNodeMap); try @@ -259,9 +349,20 @@ var logicalAst: IAstNode; begin 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; + // ^ 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; CollectNodes(FWorkspace.World); @@ -323,7 +424,7 @@ end; procedure TAstEditorController.TTypePropagator.Accept(const Node: IAstNode); begin if Node = nil then - Exit; + exit; ApplyType(Node); Node.Accept(Self); end;