diff --git a/GDK.ToolsAPI.Debugger.Interfaces.pas b/GDK.ToolsAPI.Debugger.Interfaces.pas new file mode 100644 index 0000000..6cd4e12 --- /dev/null +++ b/GDK.ToolsAPI.Debugger.Interfaces.pas @@ -0,0 +1,59 @@ +unit GDK.ToolsAPI.Debugger.Interfaces; + +interface + +uses + System.SysUtils; + +type + EToolsApiNoDebuggerServices = class(Exception); + EToolsApiEvaluateFailed = class(Exception); + + {$SCOPEDENUMS ON} + TToolsApiProcessState = (Nothing, Running, Stopping, Stopped, Fault, + ResFault, Terminated, Exception, NoProcess); + + TToolsApiStepMode = (StepOver, StepInto, RunUntilReturn); + {$SCOPEDENUMS OFF} + + TToolsApiBreakpointInfo = record + FileName: string; + LineNumber: Integer; + Enabled: Boolean; + Expression: string; + Valid: Boolean; + end; + + TToolsApiStackFrame = record + Index: Integer; + Header: string; + FileName: string; + LineNumber: Integer; + end; + + IToolsApiDebugger = interface + ['{A6A8EF47-CEC7-43BB-8F88-A0A6A12F4E47}'] + + procedure AddBreakpoint(const FileName: string; const LineNumber: Integer); + function RemoveBreakpoint(const FileName: string; const LineNumber: Integer): Boolean; + function RemoveAllBreakpoints: Integer; + function ListBreakpoints: TArray; + + function State: TToolsApiProcessState; + function HasProcess: Boolean; + procedure Continue; + procedure Step(const Mode: TToolsApiStepMode); + procedure Pause; + procedure Terminate; + + function Evaluate(const Expression: string): string; + function CallStack: TArray; + function TryGetCurrentLocation(out FileName: string; out LineNumber: Integer): Boolean; + + // Pumps pending debug events; required while waiting for a deferred evaluate. + procedure ProcessDebugEvents; + end; + +implementation + +end. diff --git a/GDK.ToolsAPI.Debugger.pas b/GDK.ToolsAPI.Debugger.pas new file mode 100644 index 0000000..d23dbd7 --- /dev/null +++ b/GDK.ToolsAPI.Debugger.pas @@ -0,0 +1,297 @@ +unit GDK.ToolsAPI.Debugger; + +interface + +uses + ToolsAPI, + GDK.ToolsAPI.Debugger.Interfaces; + +type + TToolsApiDebugger = class(TInterfacedObject, IToolsApiDebugger) + private + const EvaluateBufferSize = 8192; + const DeferredPollCount = 200; + + type TEvaluateBuffer = array[0..EvaluateBufferSize - 1] of Char; + + function DebuggerServices: IOTADebuggerServices; + function TryDebuggerServices(out Services: IOTADebuggerServices): Boolean; + function CurrentProcess: IOTAProcess; + function CurrentThread: IOTAThread; + function FindSourceBreakpoint(const FileName: string; const LineNumber: Integer): IOTASourceBreakpoint; + function MapState(const State: TOTAProcessState): TToolsApiProcessState; + procedure RunProcess(const Mode: TOTARunMode); + function EvaluateOnce(const Thread: IOTAThread; const Expression: string; out Value: string): TOTAEvaluateResult; + public + procedure AddBreakpoint(const FileName: string; const LineNumber: Integer); + function RemoveBreakpoint(const FileName: string; const LineNumber: Integer): Boolean; + function RemoveAllBreakpoints: Integer; + function ListBreakpoints: TArray; + + function State: TToolsApiProcessState; + function HasProcess: Boolean; + procedure Continue; + procedure Step(const Mode: TToolsApiStepMode); + procedure Pause; + procedure Terminate; + + function Evaluate(const Expression: string): string; + function CallStack: TArray; + function TryGetCurrentLocation(out FileName: string; out LineNumber: Integer): Boolean; + + procedure ProcessDebugEvents; + end; + +implementation + +uses + System.SysUtils; + +function TToolsApiDebugger.TryDebuggerServices(out Services: IOTADebuggerServices): Boolean; +begin + Result := Assigned(BorlandIDEServices) and + Supports(BorlandIDEServices, IOTADebuggerServices, Services); +end; + +function TToolsApiDebugger.DebuggerServices: IOTADebuggerServices; +begin + if not TryDebuggerServices(Result) then + raise EToolsApiNoDebuggerServices.Create('Debugger services unavailable'); +end; + +function TToolsApiDebugger.CurrentProcess: IOTAProcess; +begin + Result := DebuggerServices.CurrentProcess; +end; + +function TToolsApiDebugger.CurrentThread: IOTAThread; +begin + const Process = CurrentProcess; + if Assigned(Process) then + Result := Process.CurrentThread + else + Result := nil; +end; + +function TToolsApiDebugger.FindSourceBreakpoint(const FileName: string; const LineNumber: Integer): IOTASourceBreakpoint; +begin + Result := nil; + + const Services = DebuggerServices; + for var Index := 0 to Services.SourceBkptCount - 1 do + begin + const Breakpoint = Services.SourceBkpts[Index]; + const Matches = SameFileName(Breakpoint.FileName, FileName) and (Breakpoint.LineNumber = LineNumber); + if Matches then + Exit(Breakpoint); + end; +end; + +procedure TToolsApiDebugger.AddBreakpoint(const FileName: string; const LineNumber: Integer); +begin + const Existing = FindSourceBreakpoint(FileName, LineNumber); + if Assigned(Existing) then + Exit; + + DebuggerServices.NewSourceBreakpoint(FileName, LineNumber, nil); +end; + +function TToolsApiDebugger.RemoveBreakpoint(const FileName: string; const LineNumber: Integer): Boolean; +begin + const Existing = FindSourceBreakpoint(FileName, LineNumber); + Result := Assigned(Existing); + if Result then + DebuggerServices.RemoveBreakpoint(Existing); +end; + +function TToolsApiDebugger.RemoveAllBreakpoints: Integer; +begin + const Services = DebuggerServices; + + // Removing shifts the breakpoint list, so iterate backwards. + Result := Services.SourceBkptCount; + for var Index := Services.SourceBkptCount - 1 downto 0 do + begin + Services.RemoveBreakpoint(Services.SourceBkpts[Index]); + end; +end; + +function TToolsApiDebugger.ListBreakpoints: TArray; +begin + const Services = DebuggerServices; + + SetLength(Result, Services.SourceBkptCount); + for var Index := 0 to Services.SourceBkptCount - 1 do + begin + const Breakpoint = Services.SourceBkpts[Index]; + Result[Index].FileName := Breakpoint.FileName; + Result[Index].LineNumber := Breakpoint.LineNumber; + Result[Index].Enabled := Breakpoint.Enabled; + Result[Index].Expression := Breakpoint.Expression; + Result[Index].Valid := Breakpoint.ValidInCurrentProcess; + end; +end; + +function TToolsApiDebugger.MapState(const State: TOTAProcessState): TToolsApiProcessState; +begin + case State of + psNothing: Result := TToolsApiProcessState.Nothing; + psRunning: Result := TToolsApiProcessState.Running; + psStopping: Result := TToolsApiProcessState.Stopping; + psStopped: Result := TToolsApiProcessState.Stopped; + psFault: Result := TToolsApiProcessState.Fault; + psResFault: Result := TToolsApiProcessState.ResFault; + psTerminated: Result := TToolsApiProcessState.Terminated; + psException: Result := TToolsApiProcessState.Exception; + else + Result := TToolsApiProcessState.NoProcess; + end; +end; + +function TToolsApiDebugger.State: TToolsApiProcessState; +begin + var Services: IOTADebuggerServices; + if not TryDebuggerServices(Services) then + Exit(TToolsApiProcessState.NoProcess); + + const Process = Services.CurrentProcess; + if not Assigned(Process) then + Exit(TToolsApiProcessState.NoProcess); + + Result := MapState(Process.ProcessState); +end; + +function TToolsApiDebugger.HasProcess: Boolean; +begin + var Services: IOTADebuggerServices; + Result := TryDebuggerServices(Services) and Assigned(Services.CurrentProcess); +end; + +procedure TToolsApiDebugger.RunProcess(const Mode: TOTARunMode); +begin + const Process = CurrentProcess; + if not Assigned(Process) then + Exit; + + Process.Run(Mode); +end; + +procedure TToolsApiDebugger.Continue; +begin + RunProcess(ormRun); +end; + +procedure TToolsApiDebugger.Step(const Mode: TToolsApiStepMode); +begin + case Mode of + TToolsApiStepMode.StepInto: RunProcess(ormStmtStepInto); + TToolsApiStepMode.RunUntilReturn: RunProcess(ormRunUntilReturn); + else + RunProcess(ormStmtStepOver); + end; +end; + +procedure TToolsApiDebugger.Pause; +begin + const Process = CurrentProcess; + if Assigned(Process) then + Process.Pause; +end; + +procedure TToolsApiDebugger.Terminate; +begin + const Process = CurrentProcess; + if Assigned(Process) then + Process.Terminate; +end; + +function TToolsApiDebugger.Evaluate(const Expression: string): string; +begin + const Thread = CurrentThread; + if not Assigned(Thread) then + raise EToolsApiEvaluateFailed.Create('No stopped process to evaluate in'); + + var Value := ''; + var EvalResult := EvaluateOnce(Thread, Expression, Value); + + // Deferred means the evaluator has to call a function inside the debuggee; + // pump debug events until the result arrives or the attempt is abandoned. + var Polls := 0; + while (EvalResult = erDeferred) and (Polls < DeferredPollCount) do + begin + DebuggerServices.ProcessDebugEvents; + Inc(Polls); + EvalResult := EvaluateOnce(Thread, Expression, Value); + end; + + case EvalResult of + erOK: Result := Value; + erBusy: raise EToolsApiEvaluateFailed.Create('Evaluator is busy, try again'); + erDeferred: raise EToolsApiEvaluateFailed.Create('Evaluation did not complete in time'); + else + // On erError the evaluator message is what came back in the buffer. + raise EToolsApiEvaluateFailed.Create(Value); + end; +end; + +function TToolsApiDebugger.EvaluateOnce(const Thread: IOTAThread; const Expression: string; out Value: string): TOTAEvaluateResult; +begin + var Buffer: TEvaluateBuffer; + FillChar(Buffer, SizeOf(Buffer), 0); + + var CanModify := False; + var ResultAddr: LongWord := 0; + var ResultSize: LongWord := 0; + var ResultVal: LongWord := 0; + + Result := Thread.Evaluate(Expression, @Buffer[0], EvaluateBufferSize, + CanModify, True, nil, ResultAddr, ResultSize, ResultVal); + + Value := Buffer; +end; + +function TToolsApiDebugger.CallStack: TArray; +begin + Result := nil; + + const Thread = CurrentThread; + if not Assigned(Thread) then + Exit; + + // GetCallCount must precede GetCallHeader/GetCallPos; frames are 1-based. + const Count = Thread.GetCallCount; + SetLength(Result, Count); + + for var Index := 1 to Count do + begin + var FrameFile := ''; + var FrameLine := 0; + Thread.GetCallPos(Index, FrameFile, FrameLine); + + Result[Index - 1].Index := Index; + Result[Index - 1].Header := Thread.GetCallHeader(Index); + Result[Index - 1].FileName := FrameFile; + Result[Index - 1].LineNumber := FrameLine; + end; +end; + +function TToolsApiDebugger.TryGetCurrentLocation(out FileName: string; out LineNumber: Integer): Boolean; +begin + FileName := ''; + LineNumber := 0; + + const Thread = CurrentThread; + if not Assigned(Thread) then + Exit(False); + + FileName := Thread.GetCurrentFile; + LineNumber := Integer(Thread.GetCurrentLine); + Result := (FileName <> ''); +end; + +procedure TToolsApiDebugger.ProcessDebugEvents; +begin + DebuggerServices.ProcessDebugEvents; +end; + +end. diff --git a/GDK.ToolsAPI.FormCreator.pas b/GDK.ToolsAPI.FormCreator.pas index 059c6fb..dc8ddd9 100644 --- a/GDK.ToolsAPI.FormCreator.pas +++ b/GDK.ToolsAPI.FormCreator.pas @@ -6,9 +6,9 @@ interface ToolsAPI; type - // Creates a new form unit through IOTAModuleServices.CreateModule: the IDE - // generates the default unit source and .dfm, adds the unit to the owning - // project and opens the designer. + // Creates a new form unit through IOTAModuleServices.CreateModule and adds it + // to the owning project. The unit and .dfm source are generated here so the + // form class is exactly T. TToolsApiFormCreator = class(TInterfacedObject, IOTACreator, IOTAModuleCreator) private FOwner: IOTAProject; @@ -45,7 +45,8 @@ TToolsApiFormCreator = class(TInterfacedObject, IOTACreator, IOTAModuleCreator implementation uses - System.SysUtils; + System.SysUtils, + GDK.ToolsAPI.SourceFile; constructor TToolsApiFormCreator.Create(const Owner: IOTAProject; const UnitFileName: string; @@ -125,17 +126,57 @@ function TToolsApiFormCreator.GetShowSource: Boolean; end; function TToolsApiFormCreator.NewFormFile(const FormIdent: string; const AncestorIdent: string): IOTAFile; -begin - // nil: the IDE generates the default .dfm for the ancestor. - Result := nil; +const + DfmTemplate = + 'object %0:s: T%0:s'#13#10 + + ' Left = 0'#13#10 + + ' Top = 0'#13#10 + + ' Caption = ''%0:s'''#13#10 + + ' ClientHeight = 480'#13#10 + + ' ClientWidth = 640'#13#10 + + ' Color = clBtnFace'#13#10 + + ' Font.Charset = DEFAULT_CHARSET'#13#10 + + ' Font.Color = clWindowText'#13#10 + + ' Font.Height = -12'#13#10 + + ' Font.Name = ''Segoe UI'''#13#10 + + ' Font.Style = []'#13#10 + + ' TextHeight = 15'#13#10 + + 'end'#13#10; +begin + Result := TToolsApiSourceFile.Create(Format(DfmTemplate, [FormIdent])); end; function TToolsApiFormCreator.NewImplSource(const ModuleIdent: string; const FormIdent: string; const AncestorIdent: string): IOTAFile; -begin - // nil: the IDE generates the default form unit source. - Result := nil; +const + UnitTemplate = + 'unit %0:s;'#13#10 + + ''#13#10 + + 'interface'#13#10 + + ''#13#10 + + 'uses'#13#10 + + ' Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,'#13#10 + + ' Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs;'#13#10 + + ''#13#10 + + 'type'#13#10 + + ' T%1:s = class(T%2:s)'#13#10 + + ' private'#13#10 + + ' { Private declarations }'#13#10 + + ' public'#13#10 + + ' { Public declarations }'#13#10 + + ' end;'#13#10 + + ''#13#10 + + 'var'#13#10 + + ' %1:s: T%1:s;'#13#10 + + ''#13#10 + + 'implementation'#13#10 + + ''#13#10 + + '{$R *.dfm}'#13#10 + + ''#13#10 + + 'end.'#13#10; +begin + Result := TToolsApiSourceFile.Create(Format(UnitTemplate, [ModuleIdent, FormIdent, AncestorIdent])); end; function TToolsApiFormCreator.NewIntfSource(const ModuleIdent: string; diff --git a/GDK.ToolsAPI.FormEditor.pas b/GDK.ToolsAPI.FormEditor.pas index f30b628..59f1315 100644 --- a/GDK.ToolsAPI.FormEditor.pas +++ b/GDK.ToolsAPI.FormEditor.pas @@ -4,6 +4,7 @@ interface uses System.Classes, + System.SysUtils, System.TypInfo, ToolsAPI, GDK.ToolsAPI.Helper.Interfaces; @@ -16,6 +17,7 @@ TToolsApiFormEditor = class(TInterfacedObject, IToolsApiFormEditor) function NativeComponent(const Component: IOTAComponent): TComponent; procedure SetPropertyValue(const Instance: TObject; const PropertyPath: string; const Value: string); procedure SetEventHandler(const Instance: TObject; const Info: PPropInfo; const MethodName: string); + procedure SetComponentReference(const Instance: TObject; const Info: PPropInfo; const ComponentName: string); public constructor Create(const Editor: IOTAFormEditor); @@ -36,6 +38,8 @@ TToolsApiFormEditor = class(TInterfacedObject, IToolsApiFormEditor) const PropertyPath: string; const Value: string); + function CaptureImage: TBytes; + procedure ShowDesigner; procedure MarkModified; end; @@ -43,9 +47,10 @@ TToolsApiFormEditor = class(TInterfacedObject, IToolsApiFormEditor) implementation uses - System.SysUtils, Vcl.Controls, + Vcl.Forms, Vcl.Graphics, + Vcl.Imaging.pngimage, DesignIntf; constructor TToolsApiFormEditor.Create(const Editor: IOTAFormEditor); @@ -111,6 +116,17 @@ function TToolsApiFormEditor.AddComponent(const TypeName: string; raise EToolsApiComponentNotFound.CreateFmt('Container "%s" not found', [ContainerName]); end; + // Capture the native container BEFORE creating: the ToolsAPI warns that an + // IOTAComponent handle may become invalid after the form is mutated, so + // resolving it afterwards can yield a stale/wrong parent (RSP-quirk that + // lands controls on the active page instead of the requested container). + const NativeContainer = NativeComponent(Container); + const RequestedNamedContainer = (not ContainerName.IsEmpty); + if RequestedNamedContainer and (not (NativeContainer is TWinControl)) then + raise EToolsApiComponentNotFound.CreateFmt( + 'Container "%s" (%s) cannot host controls; a parent must be a TWinControl (form, panel, tab sheet, group box)', + [ContainerName, NativeContainer.ClassName]); + const Created = FEditor.CreateComponent(Container, TypeName, Left, Top, Width, Height); if not Assigned(Created) then raise EToolsApiComponentNotCreated.CreateFmt( @@ -122,12 +138,11 @@ function TToolsApiFormEditor.AddComponent(const TypeName: string; begin const Control = TControl(Result); - // CreateComponent parents a new control to the active page of a - // PageControl (or another active container) instead of the requested one; - // force the parent so the control lands on exactly the named container. - const ContainerControl = NativeComponent(Container); - if (ContainerControl is TWinControl) and (Control.Parent <> ContainerControl) then - Control.Parent := TWinControl(ContainerControl); + // Force the parent to the exact named container (using the reference we + // captured before creating), so the control lands on the right tab/page + // rather than the currently active one. + if (NativeContainer is TWinControl) and (Control.Parent <> NativeContainer) then + Control.Parent := TWinControl(NativeContainer); // Setting Parent resets the position, so apply the bounds afterwards. Control.Left := Left; @@ -198,11 +213,32 @@ procedure TToolsApiFormEditor.SetPropertyValue(const Instance: TObject; SetSetProp(Current, Info, Value); tkMethod: SetEventHandler(Current, Info, Value); + tkClass: + SetComponentReference(Current, Info, Value); else raise EToolsApiPropertyNotSupported.CreateFmt('Property "%s" has an unsupported type', [PropertyName]); end; end; +procedure TToolsApiFormEditor.SetComponentReference(const Instance: TObject; + const Info: PPropInfo; + const ComponentName: string); +begin + // Component-reference properties (ActivePage, DataSource, Images, PopupMenu, + // ...) are set by the referenced component NAME; empty means nil. + if ComponentName.IsEmpty then + begin + SetObjectProp(Instance, Info, nil); + Exit; + end; + + const Referenced = NativeComponent(FEditor.FindComponent(ComponentName)); + if not Assigned(Referenced) then + raise EToolsApiComponentNotFound.CreateFmt('Referenced component "%s" not found on the form', [ComponentName]); + + SetObjectProp(Instance, Info, Referenced); +end; + procedure TToolsApiFormEditor.SetEventHandler(const Instance: TObject; const Info: PPropInfo; const MethodName: string); @@ -235,14 +271,21 @@ procedure TToolsApiFormEditor.SetEventHandler(const Instance: TObject; (not FormDesigner.MethodFromAncestor(CurrentMethod)); if CanRename then - FormDesigner.RenameMethod(CurrentName, MethodName) - else begin - const Handler = FormDesigner.CreateMethod(MethodName, GetTypeData(Info^.PropType^)); - SetMethodProp(Instance, Info, Handler); + FormDesigner.RenameMethod(CurrentName, MethodName); + FormDesigner.Modified; + Exit; end; + const IsNewMethod = (not FormDesigner.MethodExists(MethodName)); + const Handler = FormDesigner.CreateMethod(MethodName, GetTypeData(Info^.PropType^)); + SetMethodProp(Instance, Info, Handler); FormDesigner.Modified; + + // CreateMethod only registers the handler; ShowMethod writes the empty method + // body to the source unit, as the Object Inspector does for a new event handler. + if IsNewMethod then + FormDesigner.ShowMethod(MethodName); end; function TToolsApiFormEditor.AssignedEvents(const Component: TComponent): TArray; @@ -277,6 +320,33 @@ function TToolsApiFormEditor.AssignedEvents(const Component: TComponent): TArray end; end; +function TToolsApiFormEditor.CaptureImage: TBytes; +begin + const RootComponent = Root; + if not (RootComponent is TCustomForm) then + raise EToolsApiComponentNotFound.CreateFmt('%s is not a form and cannot be captured', [RootComponent.ClassName]); + + const Bitmap = TCustomForm(RootComponent).GetFormImage; + try + const Png = TPngImage.Create; + try + Png.Assign(Bitmap); + + const Stream = TBytesStream.Create; + try + Png.SaveToStream(Stream); + Result := Copy(Stream.Bytes, 0, Stream.Size); + finally + Stream.Free; + end; + finally + Png.Free; + end; + finally + Bitmap.Free; + end; +end; + procedure TToolsApiFormEditor.ShowDesigner; begin FEditor.Show; diff --git a/GDK.ToolsAPI.Helper.Interfaces.pas b/GDK.ToolsAPI.Helper.Interfaces.pas index 324e78e..aa818e3 100644 --- a/GDK.ToolsAPI.Helper.Interfaces.pas +++ b/GDK.ToolsAPI.Helper.Interfaces.pas @@ -6,7 +6,8 @@ interface ToolsAPI, System.Classes, System.SysUtils, - GDK.ToolsAPI.CustomMessage; + GDK.ToolsAPI.CustomMessage, + GDK.ToolsAPI.Debugger.Interfaces; type IToolsApiHelper = interface; @@ -63,6 +64,7 @@ EToolsApiModuleOutOfSync = class(Exception); function BuildConfigurations: IToolsApiBuildConfigurations; function EnvironmentOptions: IToolsApiEnvironmentOptions; + function Debugger: IToolsApiDebugger; function EditView: IToolsApiEditView; end; @@ -202,6 +204,9 @@ EToolsApiModuleOutOfSync = class(Exception); const PropertyPath: string; const Value: string); + // Renders the designed form to a PNG (TCustomForm.GetFormImage). + function CaptureImage: TBytes; + procedure ShowDesigner; procedure MarkModified; end; diff --git a/GDK.ToolsAPI.Helper.pas b/GDK.ToolsAPI.Helper.pas index caafdb9..af27b68 100644 --- a/GDK.ToolsAPI.Helper.pas +++ b/GDK.ToolsAPI.Helper.pas @@ -5,6 +5,7 @@ interface uses ToolsAPI, GDK.ToolsAPI.Helper.Interfaces, + GDK.ToolsAPI.Debugger.Interfaces, System.SysUtils, GDK.ToolsAPI.CustomMessage; @@ -25,6 +26,7 @@ TToolsApiHelper = class(TInterfacedObject, IToolsApiHelper) function BuildConfigurations: IToolsApiBuildConfigurations; function EnvironmentOptions: IToolsApiEnvironmentOptions; + function Debugger: IToolsApiDebugger; function ModuleCount: Integer; function Module: IToolsApiModule; overload; @@ -200,6 +202,7 @@ implementation System.Classes, System.IOUtils, DCCStrs, + GDK.ToolsAPI.Debugger, GDK.ToolsAPI.FormCreator, GDK.ToolsAPI.FormEditor, GDK.ToolsAPI.ProjectManagerContextMenu, @@ -284,6 +287,11 @@ function TToolsApiHelper.EnvironmentOptions: IToolsApiEnvironmentOptions; Result := TToolsApiEnvironmentOptions.Create; end; +function TToolsApiHelper.Debugger: IToolsApiDebugger; +begin + Result := TToolsApiDebugger.Create; +end; + function TToolsApiHelper.EditorReader: IToolsApiEditReader; begin Result := Self.SourceEditor.Reader; diff --git a/GDK.ToolsAPI.SourceFile.pas b/GDK.ToolsAPI.SourceFile.pas new file mode 100644 index 0000000..f47de03 --- /dev/null +++ b/GDK.ToolsAPI.SourceFile.pas @@ -0,0 +1,37 @@ +unit GDK.ToolsAPI.SourceFile; + +interface + +uses + ToolsAPI; + +type + // A file supplied to the IDE for a newly created module (source verbatim). + TToolsApiSourceFile = class(TInterfacedObject, IOTAFile) + private + FSource: string; + public + constructor Create(const Source: string); + function GetSource: string; + function GetAge: TDateTime; + end; + +implementation + +constructor TToolsApiSourceFile.Create(const Source: string); +begin + inherited Create; + FSource := Source; +end; + +function TToolsApiSourceFile.GetSource: string; +begin + Result := FSource; +end; + +function TToolsApiSourceFile.GetAge: TDateTime; +begin + Result := -1; +end; + +end. diff --git a/README.md b/README.md index a3934fd..83acfa3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,11 @@ With this library we will contribute to the Delphi community and make it more si ## Content [Logging / Messages](#Logger) -[Project group and projects](#Projects) +[Project group and projects](#Projects) (incl. build, environment options, module sync) + +[Form creation and designer](#form-creation-and-designer) + +[Debugger](#debugger) [Uses manager](#uses-manager) @@ -132,6 +136,69 @@ begin end; ``` +#### Module buffer/disk sync +A file open in the IDE can be newer in memory than on disk (or vice versa when an external tool edits it). **IToolsApiModule** exposes `IsDirty`, `MatchesDisk` and `SyncWithDisk` to reason about this. `SyncWithDisk` reloads the module from disk when the buffer is unmodified, and raises `EToolsApiModuleOutOfSync` when both sides changed (a real conflict). + +```Pascal +var Module: IToolsApiModule := THelper.Module; +if not Module.MatchesDisk then + Module.SyncWithDisk; // reloads if safe, raises on conflict +``` + +## Form creation and designer + +### Creating a form unit +`IToolsApiProject.CreateFormUnit` creates a new form unit through `IOTAModuleServices.CreateModule`, adds it to the project and opens the designer. The unit and `.dfm` source are generated by the library (not left to the IDE default template, which mangles the class name), so the class is exactly `T`. `AncestorName` is without the leading `T` (empty means `TForm`). + +```Pascal +var Helper: IToolsApiHelper := TToolsApiHelper.Create; +Helper.Project.CreateFormUnit('C:\proj\FMain.pas', 'MainForm', 'TForm'); +``` + +### Editing components on a form +`IToolsApiModule.FormDesigner` returns an **IToolsApiFormEditor** for component-level access to an open form designer, like the palette and Object Inspector: + +```Pascal +var Designer := THelper.Module.FormDesigner; + +// Inspect +for var Component in Designer.Components do + ; // Component.Name, .ClassName, ... +var Events := Designer.AssignedEvents(Designer.Root); // ['OnClick=Button1Click', ...] + +// Add a control to a named container (forced to the exact parent, e.g. a TabSheet) +var Edit := Designer.AddComponent('TEdit', 'pnlDetail', 8, 8, 200, 23); + +// Set published properties by text, including nested paths, sets, events and +// component references: +Designer.SetComponentProperty('Edit1', 'Text', 'hello'); +Designer.SetComponentProperty('Edit1', 'Font.Size', '12'); +Designer.SetComponentProperty('Button1', 'OnClick', 'Button1Click'); // binds/creates handler +Designer.SetComponentProperty('PageControl1', 'ActivePage', 'TabSheet1'); // component reference +``` + +`AddComponent` captures the native container before creating and forces the new control's `Parent`, so it lands on exactly the requested tab/page (not the active one). Any installed component class works (VCL, TMS, DevExpress, ...). + +### Rendering a form to an image +`IToolsApiFormEditor.CaptureImage` renders the designed form to a PNG (via `TCustomForm.GetFormImage`), returned as `TBytes`. + +## Debugger + +`IToolsApiHelper.Debugger` returns an **IToolsApiDebugger** around `IOTADebuggerServices`: source breakpoints, process control (continue/step/pause/terminate), expression evaluation (deferred-aware) and the call stack. + +```Pascal +var Debugger := THelper.Debugger; + +Debugger.AddBreakpoint('C:\proj\FMain.pas', 42); +// ... run the project ... +if Debugger.State = TToolsApiProcessState.Stopped then +begin + var Value := Debugger.Evaluate('Customer.Name'); + for var Frame in Debugger.CallStack do + ; // Frame.Header, .FileName, .LineNumber +end; +``` + ## Uses manager The **TToolsApiUsesManager** class is located in `GDK.ToolsAPI.UsesManager.pas` and provides the following methods: