UI refactoring

This commit is contained in:
Michael Schimmel
2025-11-29 16:43:52 +01:00
parent 521d0ac28f
commit 250f950a68
2 changed files with 155 additions and 111 deletions
+122 -97
View File
@@ -29,15 +29,10 @@ const
type type
TAstViewNode = class; // Forward declaration TAstViewNode = class; // Forward declaration
// This aggregate interface encapsulates all node-specific logic.
IAstViewNodeHandler = interface IAstViewNodeHandler = interface
// Gets the original logical AST node
function GetAstNode: IAstNode; function GetAstNode: IAstNode;
// Creates the specific FMX UI for this node
procedure BuildUI(OwnerNode: TAstViewNode); procedure BuildUI(OwnerNode: TAstViewNode);
// Reconstructs the logical AST node from the FMX UI state
function ReconstructAst(OwnerNode: TAstViewNode): IAstNode; function ReconstructAst(OwnerNode: TAstViewNode): IAstNode;
// The original logical AST node
property Node: IAstNode read GetAstNode; property Node: IAstNode read GetAstNode;
end; end;
@@ -56,19 +51,15 @@ type
property Workspace: TAstWorkspace read GetWorkspace; property Workspace: TAstWorkspace read GetWorkspace;
end; end;
// Defines the layout direction for children within TAutoFitControl
TLayoutOrientation = (loVertical, loHorizontal); TLayoutOrientation = (loVertical, loHorizontal);
// Defines the alignment of children on the cross-axis
TLayoutAlignment = (laCenter, laFlush); TLayoutAlignment = (laCenter, laFlush);
// A styled control that automatically adjusts its size to fit its children, including padding.
TAutoFitControl = class(TStyledControl) TAutoFitControl = class(TStyledControl)
private private
FUpdatingOwnSize: Boolean; // Recursion guard FUpdatingOwnSize: Boolean;
FNeedRecalcSize: Boolean; // Flag for EndUpdate FNeedRecalcSize: Boolean;
FOrientation: TLayoutOrientation; // Storage for Orientation FOrientation: TLayoutOrientation;
FAlignment: TLayoutAlignment; // Storage for Alignment FAlignment: TLayoutAlignment;
procedure RecalcOwnSize; procedure RecalcOwnSize;
procedure SetOrientation(const Value: TLayoutOrientation); procedure SetOrientation(const Value: TLayoutOrientation);
procedure SetAlignment(const Value: TLayoutAlignment); procedure SetAlignment(const Value: TLayoutAlignment);
@@ -87,12 +78,9 @@ type
property Alignment: TLayoutAlignment read FAlignment write SetAlignment default TLayoutAlignment.laCenter; property Alignment: TLayoutAlignment read FAlignment write SetAlignment default TLayoutAlignment.laCenter;
end; end;
// A movable panel with a custom-painted border. Content (like title) is added externally.
TAstViewNode = class(TAutoFitControl) TAstViewNode = class(TAutoFitControl)
private private
FBackgroundColor: TAlphaColor; FBackgroundColor: TAlphaColor;
FIsDragging: Boolean;
FDownPos: TPointF;
FBorderColor: TAlphaColor; FBorderColor: TAlphaColor;
FBorderWidth: Single; FBorderWidth: Single;
FBorderRadius: Single; FBorderRadius: Single;
@@ -100,7 +88,6 @@ type
FVisualizer: IAstVisualizer; FVisualizer: IAstVisualizer;
FHandler: IAstViewNodeHandler; FHandler: IAstViewNodeHandler;
// --- Visual State Fields ---
FErrorMessage: string; FErrorMessage: string;
FTypeInfoText: string; FTypeInfoText: string;
@@ -111,17 +98,20 @@ type
procedure SetFrameless(const Value: Boolean); procedure SetFrameless(const Value: Boolean);
function GetNode: IAstNode; function GetNode: IAstNode;
// New Setters
procedure SetErrorMessage(const Value: string); procedure SetErrorMessage(const Value: string);
procedure SetTypeInfoText(const Value: string); procedure SetTypeInfoText(const Value: string);
procedure UpdateVisualState; procedure UpdateVisualState;
protected protected
procedure Paint; override; procedure Paint; override;
procedure SetupNode;
// Overrides for interaction
procedure DoMouseEnter; override;
procedure DoMouseLeave; override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override; procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override;
procedure MouseMove(Shift: TShiftState; X, Y: Single); override; procedure MouseMove(Shift: TShiftState; X, Y: Single); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override; procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override;
procedure SetupNode;
public public
constructor Create(const AVisualizer: IAstVisualizer; const AHandler: IAstViewNodeHandler); reintroduce; constructor Create(const AVisualizer: IAstVisualizer; const AHandler: IAstViewNodeHandler); reintroduce;
@@ -138,7 +128,6 @@ type
property Visualizer: IAstVisualizer read FVisualizer; property Visualizer: IAstVisualizer read FVisualizer;
property Node: IAstNode read GetNode; property Node: IAstNode read GetNode;
// --- Visual State Properties ---
property ErrorMessage: string read FErrorMessage write SetErrorMessage; property ErrorMessage: string read FErrorMessage write SetErrorMessage;
property TypeInfoText: string read FTypeInfoText write SetTypeInfoText; property TypeInfoText: string read FTypeInfoText write SetTypeInfoText;
@@ -180,7 +169,6 @@ type
property OnMouseLeave; property OnMouseLeave;
end; end;
// Helper base class to reduce boilerplate in specific handlers
TBaseNodeHandler<T: IAstNode> = class(TInterfacedObject, IAstViewNodeHandler) TBaseNodeHandler<T: IAstNode> = class(TInterfacedObject, IAstViewNodeHandler)
protected protected
FNode: T; FNode: T;
@@ -394,8 +382,6 @@ begin
FVisualizer := AVisualizer.Clone(Self, AVisualizer.ExprDepth); FVisualizer := AVisualizer.Clone(Self, AVisualizer.ExprDepth);
FHandler := AHandler; FHandler := AHandler;
FIsDragging := False;
FBorderColor := TAlphaColors.Gray; FBorderColor := TAlphaColors.Gray;
FBorderWidth := 1; FBorderWidth := 1;
FBorderRadius := 9; FBorderRadius := 9;
@@ -406,6 +392,7 @@ begin
Width := cMinNodeWidth; Width := cMinNodeWidth;
Height := cMinNodeHeight; Height := cMinNodeHeight;
// HitTest muss True sein für Hover-Effekte (IsMouseOver)
HitTest := True; HitTest := True;
ClipChildren := True; ClipChildren := True;
@@ -429,6 +416,60 @@ begin
inherited; inherited;
end; end;
procedure TAstViewNode.DoMouseEnter;
begin
inherited;
// Wir müssen neu zeichnen, da sich IsMouseOver geändert hat,
// FMX triggert Paint nicht automatisch bei Hover.
Repaint;
end;
procedure TAstViewNode.DoMouseLeave;
begin
inherited;
Repaint;
end;
procedure TAstViewNode.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
var
pt: TPointF;
begin
inherited;
// Panning-Weiterleitung an den Workspace
if Assigned(FVisualizer.Workspace) then
begin
pt := LocalToAbsolute(TPointF.Create(X, Y));
pt := FVisualizer.Workspace.AbsoluteToLocal(pt);
FVisualizer.Workspace.ExternalMouseDown(Button, Shift, pt.X, pt.Y);
end;
end;
procedure TAstViewNode.MouseMove(Shift: TShiftState; X, Y: Single);
var
pt: TPointF;
begin
inherited;
if Assigned(FVisualizer.Workspace) then
begin
pt := LocalToAbsolute(TPointF.Create(X, Y));
pt := FVisualizer.Workspace.AbsoluteToLocal(pt);
FVisualizer.Workspace.ExternalMouseMove(Shift, pt.X, pt.Y);
end;
end;
procedure TAstViewNode.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
var
pt: TPointF;
begin
inherited;
if Assigned(FVisualizer.Workspace) then
begin
pt := LocalToAbsolute(TPointF.Create(X, Y));
pt := FVisualizer.Workspace.AbsoluteToLocal(pt);
FVisualizer.Workspace.ExternalMouseUp(Button, Shift, pt.X, pt.Y);
end;
end;
function TAstViewNode.AddContainer(Parent: TControl; Orientation: TLayoutOrientation; Alignment: TLayoutAlignment): TAutoFitControl; function TAstViewNode.AddContainer(Parent: TControl; Orientation: TLayoutOrientation; Alignment: TLayoutAlignment): TAutoFitControl;
begin begin
Result := TAutoFitControl.Create(Self); Result := TAutoFitControl.Create(Self);
@@ -535,57 +576,88 @@ var
effBorderColor: TAlphaColor; effBorderColor: TAlphaColor;
effBorderWidth: Single; effBorderWidth: Single;
effDash: TStrokeDash; effDash: TStrokeDash;
isHovering: Boolean;
isBlock: Boolean;
begin begin
inherited; inherited;
Canvas.Fill.Kind := TBrushKind.None; // Check if this is a block node (blocks should not highlight on hover)
isBlock := Assigned(Node) and (Node.Kind = TAstNodeKind.akBlockExpression);
// Determine styles based on state (Error > Standard) // Determine hover state: active only if mouse is over AND it is not a block
if FErrorMessage <> '' then isHovering := IsMouseOver and (not isBlock);
// 1. Set base values
if FFrameless then
begin begin
effBorderColor := TAlphaColors.Red; effBorderColor := FBorderColor;
effBorderWidth := 2.0; effBorderWidth := FBorderWidth;
effDash := TStrokeDash.Solid; effDash := TStrokeDash.Dot;
end end
else else
begin begin
effBorderColor := FBorderColor; effBorderColor := FBorderColor;
effBorderWidth := FBorderWidth; effBorderWidth := FBorderWidth;
if FFrameless then effDash := TStrokeDash.Solid;
effDash := TStrokeDash.Dot
else
effDash := TStrokeDash.Solid;
end; end;
if FFrameless and (FErrorMessage = '') then // 2. Hover state (overrides base, makes frameless nodes visible)
if isHovering then
begin begin
// Standard frameless drawing (only if no error) // Bright white for strong contrast against dark background
Canvas.Fill.Kind := TBrushKind.Solid; effBorderColor := TAlphaColors.White;
Canvas.Fill.Color := FBackgroundColor;
Canvas.Stroke.Kind := TBrushKind.None; // Make border slightly thicker for a "pop" effect
effBorderWidth := Max(2.0, FBorderWidth + 1.0);
// Always solid line when hovering, even if originally frameless
effDash := TStrokeDash.Solid;
end;
// 3. Error state (dominates everything)
if FErrorMessage <> '' then
begin
effBorderColor := TAlphaColors.Red;
effBorderWidth := 2.0;
effDash := TStrokeDash.Solid;
end;
// --- Drawing ---
// Fill background
Canvas.Fill.Kind := TBrushKind.Solid;
Canvas.Fill.Color := FBackgroundColor;
if FFrameless and (FErrorMessage = '') and (not isHovering) then
begin
// Special case: Inactive, frameless node (e.g., simple constant)
// Draw only background, no border
Canvas.FillRect(TRectF.Create(0, 0, Width, Height), 0, 0, [], 1.0); Canvas.FillRect(TRectF.Create(0, 0, Width, Height), 0, 0, [], 1.0);
Canvas.Stroke.Kind := TBrushKind.Solid;
Canvas.Stroke.Color := effBorderColor; // Draw border only if explicit width is set (e.g. debugging)
Canvas.Stroke.Thickness := effBorderWidth; if FBorderWidth > 0 then
Canvas.Stroke.Dash := effDash; begin
Canvas.DrawRect(TRectF.Create(0, 0, Width, Height), 0, 0, [], 1.0); 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 end
else else
begin begin
// Draw background fill first // Standard case: Node with border (or hovered frameless node)
Canvas.Fill.Kind := TBrushKind.Solid;
Canvas.Fill.Color := FBackgroundColor;
rect := TRectF.Create(0, 0, Width, Height); rect := TRectF.Create(0, 0, Width, Height);
// Adjust to avoid clipping the border
if (effBorderWidth > 0) then if (effBorderWidth > 0) then
rect.Inflate(-effBorderWidth / 2, -effBorderWidth / 2); rect.Inflate(-effBorderWidth / 2, -effBorderWidth / 2);
Canvas.FillRect(rect, FBorderRadius, FBorderRadius, AllCorners, 1.0, TCornerType.Round); Canvas.FillRect(rect, FBorderRadius, FBorderRadius, AllCorners, 1.0, TCornerType.Round);
// Draw border (Custom or Error) // Draw border
if (effBorderWidth > 0) and (effBorderColor <> TAlphaColors.Null) then if (effBorderWidth > 0) and (effBorderColor <> TAlphaColors.Null) then
begin begin
rect := TRectF.Create(0, 0, Width, Height);
rect.Inflate(-effBorderWidth / 2, -effBorderWidth / 2);
Canvas.Stroke.Kind := TBrushKind.Solid; Canvas.Stroke.Kind := TBrushKind.Solid;
Canvas.Stroke.Color := effBorderColor; Canvas.Stroke.Color := effBorderColor;
Canvas.Stroke.Thickness := effBorderWidth; Canvas.Stroke.Thickness := effBorderWidth;
@@ -631,53 +703,6 @@ begin
end; end;
end; end;
procedure TAstViewNode.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
inherited;
if Button = TMouseButton.mbLeft then
begin
var lControl := ObjectAtPoint(LocalToScreen(TPointF.Create(X, Y)));
if Assigned(lControl) and (lControl.GetObject = Self) then
begin
FIsDragging := True;
FDownPos := TPointF.Create(X, Y);
Capture;
BringToFront;
end
else
FIsDragging := False;
end;
end;
procedure TAstViewNode.MouseMove(Shift: TShiftState; X, Y: Single);
begin
inherited;
if FIsDragging then
begin
var deltaX := X - FDownPos.X;
var deltaY := Y - FDownPos.Y;
Position.X := Position.X + deltaX;
Position.Y := Position.Y + deltaY;
if ParentControl <> nil then
ParentControl.Repaint;
end;
end;
procedure TAstViewNode.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
inherited;
if (Button = TMouseButton.mbLeft) and (FIsDragging) then
begin
ReleaseCapture;
FIsDragging := False;
if ParentControl <> nil then
ParentControl.Repaint;
end;
end;
procedure TAstViewNode.SetBackgroundColor(const Value: TAlphaColor); procedure TAstViewNode.SetBackgroundColor(const Value: TAlphaColor);
begin begin
FBackgroundColor := Value; FBackgroundColor := Value;
+33 -14
View File
@@ -47,6 +47,11 @@ type
function GetChildrenMatrix(var Matrix: TMatrix; var Simple: Boolean): Boolean; override; function GetChildrenMatrix(var Matrix: TMatrix; var Simple: Boolean): Boolean; override;
function Zoom(Factor: Single): Boolean; function Zoom(Factor: Single): Boolean;
// Neue Public Methoden, damit Nodes das Panning steuern können
procedure ExternalMouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
procedure ExternalMouseMove(Shift: TShiftState; X, Y: Single);
procedure ExternalMouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
published published
property Align; property Align;
property Anchors; property Anchors;
@@ -88,8 +93,8 @@ implementation
uses uses
System.Math, System.Math,
FMX.Platform, FMX.Platform,
Myc.Fmx.AstEditor.Core, // Enthält TAstViewNode Interfaces Myc.Fmx.AstEditor.Core,
Myc.Fmx.AstEditor.Visualizer; // Enthält TAstVisualizer Myc.Fmx.AstEditor.Visualizer;
{ TPinConnection } { TPinConnection }
@@ -112,7 +117,6 @@ var
visu: IAstVisualizer; visu: IAstVisualizer;
node: TAstViewNode; node: TAstViewNode;
begin begin
// Erstelle den Visualizer mit diesem Workspace als Kontext
visu := TAstVisualizer.Create(Self, Self, 0); visu := TAstVisualizer.Create(Self, Self, 0);
node := visu.CallAccept(RootNode); node := visu.CallAccept(RootNode);
if Assigned(node) then if Assigned(node) then
@@ -132,23 +136,43 @@ begin
Result := True; Result := True;
end; end;
// --- Interne Event Handler ---
procedure TAstWorkspace.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); procedure TAstWorkspace.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin begin
inherited; inherited;
if (Button = TMouseButton.mbLeft) and (ObjectAtPoint(LocalToScreen(TPointF.Create(X, Y))) = Self as IControl) then ExternalMouseDown(Button, Shift, X, Y);
end;
procedure TAstWorkspace.MouseMove(Shift: TShiftState; X, Y: Single);
begin
inherited;
ExternalMouseMove(Shift, X, Y);
end;
procedure TAstWorkspace.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
inherited;
ExternalMouseUp(Button, Shift, X, Y);
end;
// --- Externe Steuerung für Nodes ---
procedure TAstWorkspace.ExternalMouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
if (Button = TMouseButton.mbLeft) then
begin begin
Capture;
FIsPanning := True; FIsPanning := True;
FLastPanPos := TPointF.Create(X, Y); FLastPanPos := TPointF.Create(X, Y);
Cursor := crHandPoint; Cursor := crHandPoint;
end; end;
end; end;
procedure TAstWorkspace.MouseMove(Shift: TShiftState; X, Y: Single); procedure TAstWorkspace.ExternalMouseMove(Shift: TShiftState; X, Y: Single);
var var
delta: TPointF; delta: TPointF;
begin begin
inherited;
if FIsPanning then if FIsPanning then
begin begin
delta := TPointF.Create(X - FLastPanPos.X, Y - FLastPanPos.Y); delta := TPointF.Create(X - FLastPanPos.X, Y - FLastPanPos.Y);
@@ -161,11 +185,11 @@ begin
end; end;
end; end;
procedure TAstWorkspace.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single); procedure TAstWorkspace.ExternalMouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin begin
inherited;
if (Button = TMouseButton.mbLeft) and (FIsPanning) then if (Button = TMouseButton.mbLeft) and (FIsPanning) then
begin begin
ReleaseCapture;
FIsPanning := False; FIsPanning := False;
Cursor := crDefault; Cursor := crDefault;
end; end;
@@ -200,26 +224,21 @@ begin
for connection in FConnections do for connection in FConnections do
begin begin
// Output Pin Calculation
pinCenter := TPointF.Create(connection.OutputPin.Width / 2, connection.OutputPin.Height / 2); pinCenter := TPointF.Create(connection.OutputPin.Width / 2, connection.OutputPin.Height / 2);
absoluteStart := connection.OutputPin.LocalToAbsolute(pinCenter); absoluteStart := connection.OutputPin.LocalToAbsolute(pinCenter);
// Input Pin Calculation
pinCenter := TPointF.Create(connection.InputPin.Width / 2, connection.InputPin.Height / 2); pinCenter := TPointF.Create(connection.InputPin.Width / 2, connection.InputPin.Height / 2);
absoluteEnd := connection.InputPin.LocalToAbsolute(pinCenter); absoluteEnd := connection.InputPin.LocalToAbsolute(pinCenter);
// Convert to Workspace Local Coordinates
startPoint := AbsoluteToLocal(absoluteStart); startPoint := AbsoluteToLocal(absoluteStart);
endPoint := AbsoluteToLocal(absoluteEnd); endPoint := AbsoluteToLocal(absoluteEnd);
// Determine Color based on TagString (naive check)
str := connection.InputPin.TagString; str := connection.InputPin.TagString;
if Pos('data', str) = 0 then if Pos('data', str) = 0 then
Canvas.Stroke.Color := cExecPinColor Canvas.Stroke.Color := cExecPinColor
else else
Canvas.Stroke.Color := cDataPinColor; Canvas.Stroke.Color := cDataPinColor;
// Draw Curve
path := TPathData.Create; path := TPathData.Create;
try try
controlOffset := Max(25, 0.5 * endPoint.Distance(startPoint)); controlOffset := Max(25, 0.5 * endPoint.Distance(startPoint));