Undo/Redo
This commit is contained in:
@@ -25,6 +25,7 @@ type
|
||||
private
|
||||
type
|
||||
TNodeMap = TDictionary<IAstIdentity, TAstViewNode>;
|
||||
TUndoStack = TStack<IAstNode>;
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user