Focus & cursor movement in UI

This commit is contained in:
Michael Schimmel
2025-12-01 13:19:07 +01:00
parent 68a97e6985
commit 656375de99
10 changed files with 1468 additions and 867 deletions
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -29,9 +29,9 @@ uses
Myc.Fmx.AstEditor.Handlers in '..\Src\AST\Myc.Fmx.AstEditor.Handlers.pas', Myc.Fmx.AstEditor.Handlers in '..\Src\AST\Myc.Fmx.AstEditor.Handlers.pas',
Myc.Fmx.AstEditor.Visualizer in '..\Src\AST\Myc.Fmx.AstEditor.Visualizer.pas', Myc.Fmx.AstEditor.Visualizer in '..\Src\AST\Myc.Fmx.AstEditor.Visualizer.pas',
Myc.Fmx.AstEditor.Workspace in '..\Src\AST\Myc.Fmx.AstEditor.Workspace.pas', Myc.Fmx.AstEditor.Workspace in '..\Src\AST\Myc.Fmx.AstEditor.Workspace.pas',
Myc.Fmx.AstEditor.Controller in '..\Src\AST\Myc.Fmx.AstEditor.Controller.pas', Myc.Fmx.AstEditor.Layout in '..\Src\AST\Myc.Fmx.AstEditor.Layout.pas',
Myc.Fmx.AstEditor in '..\Src\AST\Myc.Fmx.AstEditor.pas', Myc.Fmx.AstEditor.Node in '..\Src\AST\Myc.Fmx.AstEditor.Node.pas',
Myc.Fmx.AstEditor.Layout in '..\Src\AST\Myc.Fmx.AstEditor.Layout.pas'; Myc.Fmx.AstEditor in '..\Src\AST\Myc.Fmx.AstEditor.pas';
{$R *.res} {$R *.res}
+2 -2
View File
@@ -159,9 +159,9 @@
<DCCReference Include="..\Src\AST\Myc.Fmx.AstEditor.Handlers.pas"/> <DCCReference Include="..\Src\AST\Myc.Fmx.AstEditor.Handlers.pas"/>
<DCCReference Include="..\Src\AST\Myc.Fmx.AstEditor.Visualizer.pas"/> <DCCReference Include="..\Src\AST\Myc.Fmx.AstEditor.Visualizer.pas"/>
<DCCReference Include="..\Src\AST\Myc.Fmx.AstEditor.Workspace.pas"/> <DCCReference Include="..\Src\AST\Myc.Fmx.AstEditor.Workspace.pas"/>
<DCCReference Include="..\Src\AST\Myc.Fmx.AstEditor.Controller.pas"/>
<DCCReference Include="..\Src\AST\Myc.Fmx.AstEditor.pas"/>
<DCCReference Include="..\Src\AST\Myc.Fmx.AstEditor.Layout.pas"/> <DCCReference Include="..\Src\AST\Myc.Fmx.AstEditor.Layout.pas"/>
<DCCReference Include="..\Src\AST\Myc.Fmx.AstEditor.Node.pas"/>
<DCCReference Include="..\Src\AST\Myc.Fmx.AstEditor.pas"/>
<BuildConfiguration Include="Base"> <BuildConfiguration Include="Base">
<Key>Base</Key> <Key>Base</Key>
</BuildConfiguration> </BuildConfiguration>
+30 -40
View File
@@ -44,9 +44,7 @@ uses
Myc.Ast.RTL, Myc.Ast.RTL,
Myc.Ast.Compiler.Macros, Myc.Ast.Compiler.Macros,
// Editor Units // Editor Units
Myc.Fmx.AstEditor, Myc.Fmx.AstEditor; // Die neue Komponente (ersetzt Controller und Workspace-Access)
Myc.Fmx.AstEditor.Workspace,
Myc.Fmx.AstEditor.Controller; // Controller integration
type type
// A test record // A test record
@@ -93,7 +91,6 @@ type
procedure ClearButtonClick(Sender: TObject); procedure ClearButtonClick(Sender: TObject);
procedure CompilerStageBoxChange(Sender: TObject); procedure CompilerStageBoxChange(Sender: TObject);
procedure FormCreate(Sender: TObject); procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure CreateTriggerExampleButtonClick(Sender: TObject); procedure CreateTriggerExampleButtonClick(Sender: TObject);
procedure DebugBoxChange(Sender: TObject); procedure DebugBoxChange(Sender: TObject);
procedure DoTrigger2ButtonClick(Sender: TObject); procedure DoTrigger2ButtonClick(Sender: TObject);
@@ -119,17 +116,16 @@ type
FCurrUnboundAst: IAstNode; FCurrUnboundAst: IAstNode;
FCurrExec: TCompiledFunction; FCurrExec: TCompiledFunction;
FEnvironment: TAstEnvironment; FEnvironment: TAstEnvironment;
FWorkspace: TWorkspace; // Changed to TWorkspace FAstEditor: TAstEditor; // Die Komponente (Facade)
FController: TAstEditorController; // The UI Controller
FScriptUpdate: Boolean; FScriptUpdate: Boolean;
FTriggerTest: TAstEnvironment; FTriggerTest: TAstEnvironment;
procedure WorkspaceMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Single); procedure AstEditorMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Single);
// Helper function to encapsulate the Compile -> Evaluate pattern // Helper function to encapsulate the Compile -> Evaluate pattern
function ExecuteAst(const ANode: IAstNode): TDataValue; function ExecuteAst(const ANode: IAstNode): TDataValue;
procedure UpdateScript; procedure UpdateScript;
procedure ShowVizualization(X, Y: Single); procedure ShowVizualization;
procedure PrintScript(const Node: IAstNode); procedure PrintScript(const Node: IAstNode);
public public
{ Public declarations } { Public declarations }
@@ -158,19 +154,20 @@ begin
// 1. Initialize Environment // 1. Initialize Environment
FEnvironment := TAstEnvironment.Construct(TAst.CreateScope(nil)); FEnvironment := TAstEnvironment.Construct(TAst.CreateScope(nil));
// 2. Initialize Workspace // 2. Initialize AST Editor Component
// TWorkspace is now a simple viewport control // This replaces the manual Workspace + Controller setup.
FWorkspace := TWorkspace.Create(Panel2); // The Editor encapsulates logic for Navigation, Zoom, Drag&Drop, Undo/Redo.
FWorkspace.Parent := Panel2; FAstEditor := TAstEditor.Create(Self);
FWorkspace.Align := TAlignLayout.Client; FAstEditor.Parent := Panel2;
FWorkspace.ClipChildren := true; FAstEditor.Align := TAlignLayout.Client;
FWorkspace.OnMouseDown := WorkspaceMouseDown;
// 3. Initialize Controller (connects Env and Workspace) // Wire up events exposed by the component
// The Controller will manage the nodes inside FWorkspace.World FAstEditor.OnMouseDown := AstEditorMouseDown;
FController := TAstEditorController.Create(FEnvironment, FWorkspace);
// 4. Register the SMA factory into the environment // Initialize the logic backend of the editor
FAstEditor.Init(FEnvironment);
// 3. Register the SMA factory into the environment
var RegFunc := var RegFunc :=
procedure(const Scope: IExecutionScope) procedure(const Scope: IExecutionScope)
var var
@@ -286,19 +283,13 @@ begin
ClearButtonClick(Self); ClearButtonClick(Self);
end; end;
procedure TForm1.FormDestroy(Sender: TObject); procedure TForm1.ShowVizualization;
begin
FController.Free;
end;
procedure TForm1.ShowVizualization(X, Y: Single);
begin begin
if FCurrUnboundAst = nil then if FCurrUnboundAst = nil then
exit; exit;
// Use the Controller to build, validate and show the UI // Use the Editor Component to build, validate and show the UI
// The controller internally clears FWorkspace.World and adds nodes there. FAstEditor.SetRoot(FCurrUnboundAst);
FController.SetRoot(FCurrUnboundAst);
end; end;
// Simplified ExecuteAst using TEnvironment and handling errors gracefully // Simplified ExecuteAst using TEnvironment and handling errors gracefully
@@ -422,14 +413,13 @@ end;
procedure TForm1.ClearButtonClick(Sender: TObject); procedure TForm1.ClearButtonClick(Sender: TObject);
begin begin
// Clear content of world (not workspace itself, as it has the root layer) // Clear content of component
FWorkspace.World.DeleteChildren; FAstEditor.SetRoot(nil);
FWorkspace.World.Repaint;
end; end;
procedure TForm1.CompilerStageBoxChange(Sender: TObject); procedure TForm1.CompilerStageBoxChange(Sender: TObject);
begin begin
ShowVizualization(14, 14); ShowVizualization;
end; end;
procedure TForm1.CreateTriggerExampleButtonClick(Sender: TObject); procedure TForm1.CreateTriggerExampleButtonClick(Sender: TObject);
@@ -731,8 +721,8 @@ begin
var result := ExecuteAst(FCurrUnboundAst); var result := ExecuteAst(FCurrUnboundAst);
Memo1.Lines.Add(Format('Script executed. Final result: %s', [result.ToString])); Memo1.Lines.Add(Format('Script executed. Final result: %s', [result.ToString]));
finally finally
// Trigger visualization update via Controller (inside ShowVizualization) // Trigger visualization update via Component
ShowVizualization(14, 14); ShowVizualization;
end; end;
except except
on E: Exception do on E: Exception do
@@ -890,15 +880,15 @@ procedure TForm1.UpdateScript;
begin begin
PrintScript(FCurrUnboundAst); PrintScript(FCurrUnboundAst);
// Clear and redraw via Controller // Clear and redraw via Component
FController.SetRoot(FCurrUnboundAst); FAstEditor.SetRoot(FCurrUnboundAst);
end; end;
procedure TForm1.WorkspaceMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Single); procedure TForm1.AstEditorMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin begin
if Button <> TMouseButton.mbMiddle then // Nur zur Demo: Wir könnten hier klicken, um etwas im Memo zu loggen
exit; if Button = TMouseButton.mbMiddle then
ShowVizualization(X, Y); ShowVizualization;
end; end;
procedure TForm1.SaveUserLibButtonClick(Sender: TObject); procedure TForm1.SaveUserLibButtonClick(Sender: TObject);
-432
View File
@@ -1,432 +0,0 @@
unit Myc.Fmx.AstEditor.Controller;
interface
uses
System.SysUtils,
System.Classes,
System.Types,
System.Math.Vectors,
System.Generics.Collections,
FMX.Types,
FMX.Controls,
Myc.Ast,
Myc.Ast.Nodes,
Myc.Ast.Identities,
Myc.Ast.Types,
Myc.Ast.Visitor,
Myc.Ast.Scope,
Myc.Ast.Environment,
Myc.Fmx.AstEditor,
Myc.Fmx.AstEditor.Workspace;
type
TAstEditorController = class
private
type
TNodeMap = TDictionary<IAstIdentity, TAstViewNode>;
TUndoStack = TStack<IAstNode>;
TTypePropagator = class(TAstVisitor)
private
FMap: TNodeMap;
procedure ApplyType(const Node: IAstNode);
protected
procedure Accept(const Node: IAstNode); override;
public
constructor Create(AMap: TNodeMap);
end;
private
FEnv: TAstEnvironment;
FWorkspace: TWorkspace;
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);
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
uses
Myc.Data.Value,
Myc.Fmx.AstEditor.Visualizer;
{ TAstEditorController }
constructor TAstEditorController.Create(const AEnv: TAstEnvironment; AWorkspace: TWorkspace);
begin
inherited Create;
FEnv := AEnv;
FWorkspace := AWorkspace;
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;
ContainerOwner: TAstViewNode;
Reorderable: IReorderable;
SourceIdx, TargetIdx: Integer;
NewAst: IAstNode;
begin
if (ASource = nil) or (AIndex < 0) then
exit;
if not (ASource is TAstViewNode) then
exit;
if not (ASource.Parent is TControl) then
exit;
SourceParent := TControl(ASource.Parent);
ContainerOwner := nil;
// Logic to find the Reorderable container (same as before)
if (SourceParent is TAstViewNode) then
begin
ContainerOwner := TAstViewNode(SourceParent);
if not Supports(ContainerOwner.Handler, IReorderable, Reorderable) then
ContainerOwner := nil;
end;
if (ContainerOwner = nil) and (SourceParent.Parent is TAstViewNode) then
begin
ContainerOwner := TAstViewNode(SourceParent.Parent);
if not Supports(ContainerOwner.Handler, IReorderable, Reorderable) then
ContainerOwner := nil;
end;
if (ContainerOwner <> nil) and (Reorderable <> nil) then
begin
if Reorderable.CanDrag(ASource) then
begin
SourceIdx := ASource.Index;
TargetIdx := AIndex;
if SourceIdx <> TargetIdx then
begin
// 1. Commit the CURRENT (clean) AST before visual changes
CommitSnapshot;
// 2. Perform Visual Change (Dirty State)
Reorderable.MoveChild(SourceIdx, TargetIdx);
// 3. Generate NEW AST from the modified visual tree
NewAst := FRootViewNode.CreateAst;
// 4. Update the system with the new AST (Updates FCurrentAst)
SetRoot(NewAst);
end;
end;
end;
end;
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;
Visualizer := TAstVisualizer.Create(FWorkspace, FWorkspace.World, 0);
FRootViewNode := Visualizer.CallAccept(AstNode);
if Assigned(FRootViewNode) then
begin
FWorkspace.World.Size := FRootViewNode.Size;
FRootViewNode.OnResized := RootResized;
FRootViewNode.Position.Point := TPointF.Create(50, 50);
end;
if Assigned(FRootViewNode) then
ValidateAndShow;
end;
// ... (Rest: CollectNodes, ClearVisuals, ApplyErrors, ApplyTypes, RootResized, TTypePropagator bleiben gleich)
procedure TAstEditorController.CollectNodes(Parent: TControl);
var
i: Integer;
child: TControl;
viewNode: TAstViewNode;
begin
for i := 0 to Parent.ChildrenCount - 1 do
begin
if Parent.Children[i] is TControl then
begin
child := TControl(Parent.Children[i]);
if child is TAstViewNode then
begin
viewNode := TAstViewNode(child);
if Assigned(viewNode.Node) and Assigned(viewNode.Node.Identity) then
begin
FNodeMap.AddOrSetValue(viewNode.Node.Identity, viewNode);
end;
end;
if child.ChildrenCount > 0 then
CollectNodes(child);
end;
end;
end;
procedure TAstEditorController.ClearVisuals;
begin
for var viewNode in FNodeMap.Values do
begin
viewNode.ErrorMessage := '';
viewNode.TypeInfoText := '';
end;
end;
procedure TAstEditorController.ApplyErrors(const Log: ICompilerLog);
var
entry: TCompilerError;
viewNode: TAstViewNode;
begin
for entry in Log.GetEntries do
begin
if (entry.Level = elError) and Assigned(entry.Node) and Assigned(entry.Node.Identity) then
begin
if FNodeMap.TryGetValue(entry.Node.Identity, viewNode) then
begin
if viewNode.ErrorMessage = '' then
viewNode.ErrorMessage := entry.Message
else
viewNode.ErrorMessage := viewNode.ErrorMessage + sLineBreak + entry.Message;
end;
end;
end;
end;
procedure TAstEditorController.ApplyTypes(const TypedAst: IAstNode);
var
propagator: TTypePropagator;
begin
if not Assigned(TypedAst) then
exit;
propagator := TTypePropagator.Create(FNodeMap);
try
propagator.Accept(TypedAst);
finally
propagator.Free;
end;
end;
procedure TAstEditorController.RootResized(Sender: TObject);
begin
FWorkspace.World.Size := FRootViewNode.Size;
end;
procedure TAstEditorController.ValidateAndShow;
var
logicalAst: IAstNode;
begin
if not Assigned(FRootViewNode) then
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);
ClearVisuals;
try
var log := TCompilerLog.Create as ICompilerLog;
var expanded := FEnv.ExpandMacros(logicalAst);
var typedAst := FEnv.Bind(expanded, [], log);
var specAst := FEnv.Specialize(typedAst);
ApplyTypes(specAst);
if specAst.Kind = akLambdaExpression then
var compiled := FEnv.Link(specAst.AsLambdaExpression, log);
ApplyErrors(log);
except
on E: ECompilationFailed do
begin
var tempLog := TCompilerLog.Create as ICompilerLog;
for var err in E.Errors do
tempLog.Add(err.Level, err.Message, err.Node);
ApplyErrors(tempLog);
end;
end;
FWorkspace.World.Repaint;
end;
{ TAstEditorController.TTypePropagator }
constructor TAstEditorController.TTypePropagator.Create(AMap: TNodeMap);
begin
inherited Create;
FMap := AMap;
end;
procedure TAstEditorController.TTypePropagator.ApplyType(const Node: IAstNode);
var
viewNode: TAstViewNode;
begin
if Assigned(Node) and Assigned(Node.Identity) then
begin
if FMap.TryGetValue(Node.Identity, viewNode) then
begin
if Node.IsTyped then
begin
var st := Node.AsTypedNode.StaticType;
if Assigned(st) then
viewNode.TypeInfoText := st.ToString;
end;
end;
end;
end;
procedure TAstEditorController.TTypePropagator.Accept(const Node: IAstNode);
begin
if Node = nil then
exit;
ApplyType(Node);
Node.Accept(Self);
end;
end.
+1 -1
View File
@@ -18,7 +18,7 @@ uses
Myc.Ast.Nodes, Myc.Ast.Nodes,
Myc.Ast.Identities, Myc.Ast.Identities,
Myc.Fmx.AstEditor.Layout, Myc.Fmx.AstEditor.Layout,
Myc.Fmx.AstEditor; Myc.Fmx.AstEditor.Node;
type type
// --- Abstract List Base --- // --- Abstract List Base ---
+542
View File
@@ -0,0 +1,542 @@
unit Myc.Fmx.AstEditor.Node;
interface
uses
System.SysUtils,
System.Classes,
System.UITypes,
System.Types,
System.Math,
System.Math.Vectors,
System.Generics.Defaults,
System.Generics.Collections,
FMX.Types,
FMX.Controls,
FMX.Objects,
FMX.Graphics,
FMX.StdCtrls,
Myc.Ast.Nodes,
Myc.Fmx.AstEditor.Workspace,
Myc.Fmx.AstEditor.Layout;
const
cNodePadding = 10;
cTitleTopPadding = 2;
cTitleBottomPadding = 4;
cMinNodeWidth = 10;
cMinNodeHeight = 10;
cDragGutterWidth = 20; // Width of the trigger zone for dragging
type
TAstViewNode = class;
IAstViewNodeHandler = interface
function GetAstNode: IAstNode;
procedure BuildUI(OwnerNode: TAstViewNode);
function ReconstructAst(OwnerNode: TAstViewNode): IAstNode;
property Node: IAstNode read GetAstNode;
end;
IReorderable = interface
['{9FA7C504-D327-4E00-A5FE-DD2876886212}']
function CanDrag(Child: TControl): Boolean;
procedure MoveChild(SourceIndex, TargetIndex: Integer);
end;
IAstVisualizer = interface
{$region 'private'}
function GetExprDepth: Integer;
function GetParentControl: TControl;
function GetWorkspace: TWorkspace;
{$endregion}
function Clone(ParentControl: TControl; ExprDepth: Integer): IAstVisualizer;
function CallAccept(const Node: IAstNode): TAstViewNode;
property ExprDepth: Integer read GetExprDepth;
property ParentControl: TControl read GetParentControl;
property Workspace: TWorkspace read GetWorkspace;
end;
TAstViewNode = class(TAutoFitControl)
private
FBackgroundColor: TAlphaColor;
FBorderColor: TAlphaColor;
FBorderWidth: Single;
FBorderRadius: Single;
FFrameless: Boolean;
FVisualizer: IAstVisualizer;
FHandler: IAstViewNodeHandler;
FErrorMessage: string;
FTypeInfoText: string;
FIsFocused: Boolean;
procedure SetBackgroundColor(const Value: TAlphaColor);
procedure SetBorderColor(const Value: TAlphaColor);
procedure SetBorderWidth(const Value: Single);
procedure SetBorderRadius(const Value: Single);
procedure SetFrameless(const Value: Boolean);
function GetNode: IAstNode;
procedure SetErrorMessage(const Value: string);
procedure SetTypeInfoText(const Value: string);
procedure UpdateVisualState;
procedure SetIsFocused(const Value: Boolean);
protected
procedure Paint; override;
procedure SetupNode;
procedure DoMouseEnter; override;
procedure DoMouseLeave; 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;
public
constructor Create(const AVisualizer: IAstVisualizer; const AHandler: IAstViewNodeHandler); reintroduce;
destructor Destroy; override;
procedure AfterConstruction; override;
function AddLabel(Parent: TControl; const Txt: String): TLabel;
function AddContainer(Parent: TControl; Orientation: TLayoutOrientation; Alignment: TLayoutAlignment): TAutoFitControl;
function AddExpr(Parent: TControl; const Title: String; const Node: IAstNode): TAstViewNode;
function CreateAst: IAstNode;
property Visualizer: IAstVisualizer read FVisualizer;
property Node: IAstNode read GetNode;
property Handler: IAstViewNodeHandler read FHandler;
property ErrorMessage: string read FErrorMessage write SetErrorMessage;
property TypeInfoText: string read FTypeInfoText write SetTypeInfoText;
property IsFocused: Boolean read FIsFocused write SetIsFocused;
published
property BorderColor: TAlphaColor read FBorderColor write SetBorderColor;
property BorderWidth: Single read FBorderWidth write SetBorderWidth;
property BorderRadius: Single read FBorderRadius write SetBorderRadius;
property BackgroundColor: TAlphaColor read FBackgroundColor write SetBackgroundColor;
property Frameless: Boolean read FFrameless write SetFrameless;
property Align;
property Anchors;
property ClipChildren default True;
property Cursor default crDefault;
property DragMode default TDragMode.dmManual;
property Enabled;
property Height;
property Hint;
property HitTest default True;
property Locked;
property Margins;
property Opacity;
property Padding;
property PopupMenu;
property Position;
property RotationAngle;
property RotationCenter;
property Scale;
property Size;
property Visible;
property Width;
property OnClick;
property OnDblClick;
property OnMouseDown;
property OnMouseMove;
property OnMouseUp;
property OnMouseWheel;
property OnMouseEnter;
property OnMouseLeave;
end;
TBaseNodeHandler<T: IAstNode> = class(TInterfacedObject, IAstViewNodeHandler)
protected
FNode: T;
function GetAstNode: IAstNode;
public
constructor Create(const ANode: T);
procedure BuildUI(OwnerNode: TAstViewNode); virtual; abstract;
function ReconstructAst(OwnerNode: TAstViewNode): IAstNode; virtual; abstract;
end;
implementation
uses
Myc.Ast;
{ TAstViewNode }
constructor TAstViewNode.Create(const AVisualizer: IAstVisualizer; const AHandler: IAstViewNodeHandler);
begin
inherited Create(AVisualizer.Workspace);
Parent := AVisualizer.ParentControl;
FVisualizer := AVisualizer.Clone(Self, AVisualizer.ExprDepth);
FHandler := AHandler;
FBorderColor := TAlphaColors.Gray;
FBorderWidth := 1;
FBorderRadius := 9;
FBackgroundColor := $080a0a0a;
FFrameless := False;
FIsFocused := False;
Width := cMinNodeWidth;
Height := cMinNodeHeight;
HitTest := True;
ClipChildren := True;
Margins.Left := 4;
Margins.Top := 4;
Margins.Right := 4;
Margins.Bottom := 4;
Padding.Left := 3;
Padding.Top := 3;
Padding.Right := 3;
Padding.Bottom := 3;
var cn: Cardinal := $ea - (7 * FVisualizer.ExprDepth);
var c: Cardinal := $ff000000 or (cn shl 16) or (cn shl 8) or cn;
BackgroundColor := c;
end;
destructor TAstViewNode.Destroy;
begin
FHandler := nil;
inherited;
end;
procedure TAstViewNode.DoMouseEnter;
begin
inherited;
Repaint;
end;
procedure TAstViewNode.DoMouseLeave;
begin
inherited;
Repaint;
end;
procedure TAstViewNode.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
const
cHeaderHeight = 30; // Defined height for header drag area
begin
// Check for Drag Start.
// Condition: Left Click AND we are part of a container (list)
if (Button = TMouseButton.mbLeft) and (Parent is TAutoFitControl) then
begin
// Hit-Test: Either Gutter (Left) OR Header area (Top)
if (X < cDragGutterWidth) or (Y < cHeaderHeight) then
begin
if Assigned(FVisualizer.Workspace) then
begin
FVisualizer.Workspace.BeginDragNode(Self);
Exit;
end;
end;
end;
inherited;
end;
procedure TAstViewNode.MouseMove(Shift: TShiftState; X, Y: Single);
const
cHeaderHeight = 30;
begin
// Cursor Logic (Hover Feedback)
if (Parent is TAutoFitControl) and ((X < cDragGutterWidth) or (Y < cHeaderHeight)) then
Cursor := crSizeAll
else
Cursor := crDefault;
inherited;
end;
procedure TAstViewNode.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
inherited;
end;
function TAstViewNode.AddContainer(Parent: TControl; Orientation: TLayoutOrientation; Alignment: TLayoutAlignment): TAutoFitControl;
begin
Result := TAutoFitControl.Create(Self);
Result.Parent := Parent;
Result.Orientation := Orientation;
Result.Alignment := Alignment;
Result.HitTest := False;
end;
function TAstViewNode.AddExpr(Parent: TControl; const Title: String; const Node: IAstNode): TAstViewNode;
begin
var cont := AddContainer(Parent, loHorizontal, laCenter);
var lbl := AddLabel(cont, Title);
lbl.Font.Style := lbl.Font.Style + [TFontStyle.fsBold];
Result := FVisualizer.Clone(cont, FVisualizer.ExprDepth + 1).CallAccept(Node);
end;
function TAstViewNode.AddLabel(Parent: TControl; const Txt: String): TLabel;
begin
Result := TLabel.Create(Self);
Result.Parent := Parent;
Result.Position.Point := TPoint.Create(cNodePadding, cNodePadding);
Result.Margins.Left := cNodePadding;
Result.Margins.Right := cNodePadding;
Result.Margins.Top := cTitleTopPadding;
Result.Margins.Bottom := cTitleBottomPadding;
Result.WordWrap := False;
Result.AutoSize := True;
Result.HitTest := False;
Result.StyledSettings := Result.StyledSettings - [TStyledSetting.Style];
Result.Text := Txt;
Result.ApplyStyleLookup;
end;
procedure TAstViewNode.AfterConstruction;
begin
inherited;
SetupNode;
end;
function TAstViewNode.CreateAst: IAstNode;
begin
if Assigned(FHandler) then
Result := FHandler.ReconstructAst(Self)
else
Result := TAst.Nop;
end;
function TAstViewNode.GetNode: IAstNode;
begin
if Assigned(FHandler) then
Result := FHandler.Node
else
Result := nil;
end;
procedure TAstViewNode.SetErrorMessage(const Value: string);
begin
if FErrorMessage <> Value then
begin
FErrorMessage := Value;
UpdateVisualState;
Repaint;
end;
end;
procedure TAstViewNode.SetTypeInfoText(const Value: string);
begin
if FTypeInfoText <> Value then
begin
FTypeInfoText := Value;
UpdateVisualState;
end;
end;
procedure TAstViewNode.SetIsFocused(const Value: Boolean);
begin
if FIsFocused <> Value then
begin
FIsFocused := Value;
Repaint;
end;
end;
procedure TAstViewNode.UpdateVisualState;
begin
if FErrorMessage <> '' then
begin
Hint := 'Error: ' + FErrorMessage;
ShowHint := True;
end
else if FTypeInfoText <> '' then
begin
Hint := 'Type: ' + FTypeInfoText;
ShowHint := True;
end
else
begin
Hint := '';
ShowHint := False;
end;
end;
procedure TAstViewNode.Paint;
var
rect: TRectF;
effBorderColor: TAlphaColor;
effBorderWidth: Single;
effDash: TStrokeDash;
isHovering: Boolean;
shouldIgnoreHover: Boolean;
begin
inherited;
shouldIgnoreHover := False;
if Assigned(Node) then
begin
case Node.Kind of
akBlockExpression, akParameterList, akArgumentList, akExpressionList, akRecordFieldList: shouldIgnoreHover := True;
end;
end;
isHovering := IsMouseOver and (not shouldIgnoreHover);
// 1. Determine Base Style
if FFrameless then
begin
effBorderColor := FBorderColor;
effBorderWidth := FBorderWidth;
effDash := TStrokeDash.Dot;
end
else
begin
effBorderColor := FBorderColor;
effBorderWidth := FBorderWidth;
effDash := TStrokeDash.Solid;
end;
// 2. Hover Overlay
if isHovering then
begin
effBorderColor := TAlphaColors.White;
effBorderWidth := Max(2.0, FBorderWidth + 1.0);
effDash := TStrokeDash.Solid;
end;
// 3. Error Overlay (High Priority for Color)
if FErrorMessage <> '' then
begin
effBorderColor := TAlphaColors.Red;
effBorderWidth := 2.0;
effDash := TStrokeDash.Solid;
end;
// 4. Focus Overlay (Highest Priority for Visibility/Color)
// Note: If Error is present, Focus will override color to Blue to indicate "Active Cursor".
// Error text is available in Hint.
if FIsFocused then
begin
effBorderColor := TAlphaColors.Dodgerblue;
effBorderWidth := 2.0;
effDash := TStrokeDash.Solid;
end;
Canvas.Fill.Kind := TBrushKind.Solid;
Canvas.Fill.Color := FBackgroundColor;
if FFrameless and (FErrorMessage = '') and (not isHovering) and (not FIsFocused) then
begin
Canvas.FillRect(TRectF.Create(0, 0, Width, Height), 0, 0, [], 1.0);
if FBorderWidth > 0 then
begin
Canvas.Stroke.Kind := TBrushKind.Solid;
Canvas.Stroke.Color := effBorderColor;
Canvas.Stroke.Thickness := effBorderWidth;
Canvas.Stroke.Dash := effDash;
Canvas.DrawRect(TRectF.Create(0, 0, Width, Height), 0, 0, [], 1.0);
end;
end
else
begin
rect := TRectF.Create(0, 0, Width, Height);
if (effBorderWidth > 0) then
rect.Inflate(-effBorderWidth / 2, -effBorderWidth / 2);
Canvas.FillRect(rect, FBorderRadius, FBorderRadius, AllCorners, 1.0, TCornerType.Round);
if (effBorderWidth > 0) and (effBorderColor <> TAlphaColors.Null) then
begin
Canvas.Stroke.Kind := TBrushKind.Solid;
Canvas.Stroke.Color := effBorderColor;
Canvas.Stroke.Thickness := effBorderWidth;
Canvas.Stroke.Dash := effDash;
Canvas.DrawRect(rect, FBorderRadius, FBorderRadius, AllCorners, 1.0, TCornerType.Round);
end;
end;
if isHovering and (Parent is TAutoFitControl) then
begin
Canvas.Fill.Color := TAlphaColors.DimGray;
Canvas.FillEllipse(TRectF.Create(4, 6, 8, 10), 1.0);
Canvas.FillEllipse(TRectF.Create(4, 12, 8, 16), 1.0);
Canvas.FillEllipse(TRectF.Create(4, 18, 8, 22), 1.0);
end;
end;
procedure TAstViewNode.SetBorderColor(const Value: TAlphaColor);
begin
if FBorderColor <> Value then
begin
FBorderColor := Value;
Repaint;
end;
end;
procedure TAstViewNode.SetBorderRadius(const Value: Single);
begin
if FBorderRadius <> Value then
begin
FBorderRadius := Value;
Repaint;
end;
end;
procedure TAstViewNode.SetBorderWidth(const Value: Single);
begin
if FBorderWidth <> Value then
begin
FBorderWidth := Value;
Repaint;
end;
end;
procedure TAstViewNode.SetFrameless(const Value: Boolean);
begin
if FFrameless <> Value then
begin
FFrameless := Value;
Repaint;
end;
end;
procedure TAstViewNode.SetBackgroundColor(const Value: TAlphaColor);
begin
FBackgroundColor := Value;
Repaint;
end;
procedure TAstViewNode.SetupNode;
begin
if Assigned(FHandler) then
FHandler.BuildUI(Self);
end;
{ TBaseNodeHandler<T> }
constructor TBaseNodeHandler<T>.Create(const ANode: T);
begin
inherited Create;
FNode := ANode;
end;
function TBaseNodeHandler<T>.GetAstNode: IAstNode;
begin
Result := FNode;
end;
end.
+1 -1
View File
@@ -10,7 +10,7 @@ uses
Myc.Ast.Nodes, Myc.Ast.Nodes,
Myc.Ast.Visitor, Myc.Ast.Visitor,
Myc.Fmx.AstEditor.Workspace, Myc.Fmx.AstEditor.Workspace,
Myc.Fmx.AstEditor, Myc.Fmx.AstEditor.Node,
Myc.Fmx.AstEditor.Handlers; Myc.Fmx.AstEditor.Handlers;
type type
+7 -2
View File
@@ -18,7 +18,6 @@ uses
type type
TOnPaintConnectionsEvent = procedure(ASender: TObject; ACanvas: TCanvas) of object; TOnPaintConnectionsEvent = procedure(ASender: TObject; ACanvas: TCanvas) of object;
TOnDragDropEvent = procedure(ASource, ATarget: TControl; AIndex: Integer) of object; TOnDragDropEvent = procedure(ASource, ATarget: TControl; AIndex: Integer) of object;
TAstWorld = class(TControl) TAstWorld = class(TControl)
@@ -29,7 +28,7 @@ type
FDropP1, FDropP2: TPointF; FDropP1, FDropP2: TPointF;
protected protected
procedure Paint; override; procedure Paint; override;
procedure AfterPaint; override; // <--- HIER: Für Zeichnen ÜBER den Kindern procedure AfterPaint; override;
public public
constructor Create(AOwner: TComponent); override; constructor Create(AOwner: TComponent); override;
procedure InvalidateConnections; procedure InvalidateConnections;
@@ -163,6 +162,8 @@ begin
inherited Create(AOwner); inherited Create(AOwner);
ClipChildren := True; ClipChildren := True;
HitTest := True; HitTest := True;
CanFocus := True; // Enable Keyboard Input
AutoCapture := True;
FRootLayer := TAstWorld.Create(Self); FRootLayer := TAstWorld.Create(Self);
FRootLayer.Parent := Self; FRootLayer.Parent := Self;
@@ -369,6 +370,10 @@ end;
procedure TWorkspace.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); procedure TWorkspace.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin begin
inherited; inherited;
// Capture Keyboard Focus
if CanFocus then
SetFocus;
if not FIsDraggingNode and (Button in [TMouseButton.mbLeft, TMouseButton.mbMiddle]) then if not FIsDraggingNode and (Button in [TMouseButton.mbLeft, TMouseButton.mbMiddle]) then
begin begin
FIsPanning := True; FIsPanning := True;
File diff suppressed because it is too large Load Diff