Skip to content

Commit df09d39

Browse files
committed
feat(standalone): TProgressBar + Cancel + worker callback + scan limit
The standalone GUI was passing nil as the progress callback to TStaticAnalyzer2.AnalyzeLeaksRecursive / AnalyzeLeaksFromList, so recursive scans gave no visual feedback beyond a static status text and a crHourglass cursor. A 100k-file folder ran unbounded with no way to abort. Mirror the IDE-plugin's progress pipeline into uMainForm: - New runtime widgets in FormCreate: FProgressBar (alClient in StatusBar1) + FBtnCancel (alRight, Width=80) - BeginAnalysisUI / EndAnalysisUI helpers - Marquee when Total unknown, Normal+Max when known; cursor + cancel-button-enabled bookkeeping - ProgressCallback handles both phases with the same throttle (100ms) + style-switch logic as uIDEAnalyseRunner: first File-Phase tick always passes the throttle so Marquee->Normal is immediate - MAX_SCAN_FILES=20000 limit aborts a runaway scan-phase - Cancel raises EAbort via FCancelRequested check on each callback Wired into three analysis entry points: - AnalyseAllClasses (recursive): BeginAnalysisUI(0) - Marquee until File-Phase - BtnBranchClick (VCS diff): BeginAnalysisUI(Files.Count) - direct File-Phase with known Max - AnalyseSingleFile: BeginAnalysisUI(0) - Marquee for the duration (no per-file callback since it's one file) Delphi nit: TProc<Integer,Integer> is "reference to procedure", not "procedure of object" - method references go through a lambda wrapper: procedure(C,T) begin ProgressCallback(C,T); end.
1 parent 2512f4d commit df09d39

1 file changed

Lines changed: 171 additions & 18 deletions

File tree

StaticCodeAnalyserForm/sources/UI/uMainForm.pas

Lines changed: 171 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ TForm2 = class(TForm)
9191
// Hint-Panel rechts vom Grid (Before/After-Code-Beispiele).
9292
// Standalone-Modus: AlwaysVisible=True (kein Auto-Hide).
9393
FHintPanel : TFindingHintPanel;
94+
// Progress-Feedback waehrend Analyse (analog zum IDE-Plugin).
95+
// ProgressBar in der StatusBar eingebettet, Cancel-Button daneben.
96+
// Werden zur Laufzeit erzeugt - kein DFM-Eintrag noetig.
97+
FProgressBar : TProgressBar;
98+
FBtnCancel : TButton;
99+
FCancelRequested: Boolean;
100+
FLastProgressTick: Cardinal;
94101
// Wendet die Display-Filter (Severity-Combo / Type-Combo / Search)
95102
// auf FAllFindings an und fuellt FDisplayedFindings + Grid neu.
96103
procedure ApplyFilter;
@@ -115,6 +122,15 @@ TForm2 = class(TForm)
115122
function AppPath: string;
116123
function RecentIniPath: string;
117124
procedure NavigateDelphiToLine(LineNo: Integer);
125+
// Cancel-Handler fuer den Standalone-Analyse-Lauf.
126+
procedure BtnCancelClick(Sender: TObject);
127+
// Worker-Callback aus AnalyzeLeaksRecursive / AnalyzeLeaksFromList.
128+
// Total < 0 -> Scan-Phase (Marquee)
129+
// Total >= 0 -> File-Phase (Normal, Position=Current)
130+
procedure ProgressCallback(Current, Total: Integer);
131+
// UI-Zustand vor / nach einem Analyse-Lauf.
132+
procedure BeginAnalysisUI(KnownTotal: Integer);
133+
procedure EndAnalysisUI;
118134
public
119135
end;
120136

@@ -295,9 +311,127 @@ procedure TForm2.FormCreate(Sender: TObject);
295311
// 1/3-Breite anpasst (und die Vorher/Nachher-Aufteilung neu rechnet).
296312
Self.OnResize := FormResizeHandler;
297313

314+
// ---- ProgressBar + Cancel-Button in der StatusBar (Laufzeit-Widgets)
315+
// Layout: Cancel-Button rechts am StatusBar-Rand (alRight, Width=80),
316+
// ProgressBar fuellt den restlichen Platz (alClient). Beide sind
317+
// initial leer/inaktiv und werden in BeginAnalysisUI/EndAnalysisUI
318+
// gesteuert. Visible=True bleibt konstant (kein Flicker).
319+
FBtnCancel := TButton.Create(Self);
320+
FBtnCancel.Parent := StatusBar1;
321+
FBtnCancel.Caption := _('Cancel');
322+
FBtnCancel.Width := 80;
323+
FBtnCancel.Align := alRight;
324+
FBtnCancel.Enabled := False;
325+
FBtnCancel.OnClick := BtnCancelClick;
326+
327+
FProgressBar := TProgressBar.Create(Self);
328+
FProgressBar.Parent := StatusBar1;
329+
FProgressBar.Align := alClient;
330+
FProgressBar.Min := 0;
331+
FProgressBar.Max := 100;
332+
FProgressBar.Position:= 0;
333+
FProgressBar.Smooth := True;
334+
FProgressBar.Style := pbstNormal;
335+
298336
LoadRecentPaths;
299337
end;
300338

339+
procedure TForm2.BtnCancelClick(Sender: TObject);
340+
begin
341+
FCancelRequested := True;
342+
// Sofort sperren - verhindert Doppelklick-Spam bevor der naechste
343+
// Callback den Abort durchfuehrt.
344+
FBtnCancel.Enabled := False;
345+
end;
346+
347+
procedure TForm2.BeginAnalysisUI(KnownTotal: Integer);
348+
begin
349+
FCancelRequested := False;
350+
FLastProgressTick := 0;
351+
Screen.Cursor := crAppStart;
352+
FBtnCancel.Enabled := True;
353+
if KnownTotal > 0 then
354+
begin
355+
FProgressBar.Style := pbstNormal;
356+
FProgressBar.Max := KnownTotal;
357+
end
358+
else
359+
begin
360+
// Scan-Phase: Marquee bis der erste File-Phase-Callback kommt.
361+
FProgressBar.Style := pbstMarquee;
362+
FProgressBar.Max := 100;
363+
end;
364+
FProgressBar.Position := 0;
365+
end;
366+
367+
procedure TForm2.EndAnalysisUI;
368+
begin
369+
FBtnCancel.Enabled := False;
370+
FProgressBar.Style := pbstNormal;
371+
FProgressBar.Position := 0;
372+
Screen.Cursor := crDefault;
373+
end;
374+
375+
procedure TForm2.ProgressCallback(Current, Total: Integer);
376+
// Wird vom Analyzer-Worker aufgerufen. Total<0 = Scan-Phase, sonst File-Phase.
377+
// Throttle auf ~10/s damit das UI nicht ueberflutet wird.
378+
const
379+
MAX_SCAN_FILES = 20000;
380+
var
381+
tick : Cardinal;
382+
doUpdate : Boolean;
383+
begin
384+
if FCancelRequested then
385+
Abort;
386+
387+
tick := GetTickCount;
388+
doUpdate := (tick - FLastProgressTick > 100);
389+
390+
// Defensiv: erster File-Phase-Tick (Total>=0) MUSS durch, damit der
391+
// Style-Switch Marquee->Normal nicht durch den 100ms-Throttle verzoegert
392+
// wird. Gleiche Logik wie im IDE-Plugin (uIDEAnalyseRunner).
393+
if (Total >= 0) and (FProgressBar.Style = pbstMarquee) then
394+
doUpdate := True;
395+
396+
if Total < 0 then
397+
begin
398+
// ---- Scan-Phase ----
399+
if Current > MAX_SCAN_FILES then
400+
begin
401+
StatusBar1.Panels[2].Text := Format(
402+
_('More than %d files found - scan cancelled.'), [MAX_SCAN_FILES]);
403+
Abort;
404+
end;
405+
if doUpdate then
406+
begin
407+
FLastProgressTick := tick;
408+
if FProgressBar.Style <> pbstMarquee then
409+
FProgressBar.Style := pbstMarquee;
410+
StatusBar1.Panels[2].Text := Format(_('Scanning... %d found'), [Current]);
411+
Application.ProcessMessages;
412+
end;
413+
end
414+
else
415+
begin
416+
// ---- File-Phase ----
417+
if doUpdate or (Current = Total) then
418+
begin
419+
FLastProgressTick := tick;
420+
if FProgressBar.Style <> pbstNormal then
421+
FProgressBar.Style := pbstNormal;
422+
if (FProgressBar.Max <> Total) and (Total > 0) then
423+
FProgressBar.Max := Total;
424+
FProgressBar.Position := Current;
425+
if Total > 0 then
426+
StatusBar1.Panels[2].Text := Format(_('File %d / %d (%d%%)'),
427+
[Current, Total, Round(Current * 100 / Total)])
428+
else
429+
StatusBar1.Panels[2].Text := Format(_('File %d'), [Current]);
430+
Application.ProcessMessages;
431+
end;
432+
end;
433+
end;
434+
301435
procedure TForm2.FormResizeHandler(Sender: TObject);
302436
begin
303437
if Assigned(FHintPanel) then FHintPanel.ApplyLayout;
@@ -515,7 +649,6 @@ procedure TForm2.AnalyseAllClasses(Sender: TObject; const path: string);
515649
Settings: TRepoSettings;
516650
findings: TObjectList<TLeakFinding>;
517651
begin
518-
Screen.Cursor := crHourglass;
519652
Settings := TRepoSettings.Create;
520653
try
521654
try Settings.Load; except end;
@@ -525,15 +658,27 @@ procedure TForm2.AnalyseAllClasses(Sender: TObject; const path: string);
525658
ApplyDetectorConfig(Settings, True);
526659

527660
StatusBar1.Panels[2].Text := _('Checking all classes...');
661+
BeginAnalysisUI(0); // Total unbekannt -> Marquee-Phase
528662
Application.ProcessMessages;
529663

530-
// Frueher: TStaticAnalyzer.AnalyzeAllClassesRecursive (uParser-basiert,
531-
// nur MemoryLeak + EmptyExcept). Jetzt: TStaticAnalyzer2 ueber alle 21
532-
// Detektoren - dieselbe Pipeline wie "Aktuelle Datei" und das IDE-Plugin.
533-
findings := TStaticAnalyzer2.AnalyzeLeaksRecursive(path,
534-
nil, Settings.UsesCheck);
664+
findings := nil;
535665
try
536-
FillGridFromFindings(findings, path);
666+
try
667+
// Frueher: TStaticAnalyzer.AnalyzeAllClassesRecursive (uParser-basiert,
668+
// nur MemoryLeak + EmptyExcept). Jetzt: TStaticAnalyzer2 ueber alle 21
669+
// Detektoren - dieselbe Pipeline wie "Aktuelle Datei" und das IDE-Plugin.
670+
findings := TStaticAnalyzer2.AnalyzeLeaksRecursive(path,
671+
procedure(C, T: Integer) begin ProgressCallback(C, T); end,
672+
Settings.UsesCheck);
673+
FillGridFromFindings(findings, path);
674+
except
675+
on EAbort do
676+
// User-Cancel oder MAX_SCAN_FILES-Limit. StatusBar wurde im
677+
// ProgressCallback bereits gesetzt.
678+
;
679+
on E: Exception do
680+
StatusBar1.Panels[2].Text := _('Analysis error: ') + E.Message;
681+
end;
537682
finally
538683
findings.Free;
539684
end;
@@ -543,7 +688,7 @@ procedure TForm2.AnalyseAllClasses(Sender: TObject; const path: string);
543688
try Settings.PersistDiscoveredClasses; except end;
544689
finally
545690
Settings.Free;
546-
Screen.Cursor := crDefault;
691+
EndAnalysisUI;
547692
end;
548693
end;
549694

@@ -564,8 +709,10 @@ procedure TForm2.AnalyseSingleFile(const AFilePath: string);
564709
// nil-init ist wichtig: wenn AnalyzeLeaks crasht BEVOR die Liste
565710
// zugewiesen wird, sehen wir ungueltigen Speicher im finally.
566711
findings := nil;
567-
Screen.Cursor := crHourglass;
568712
Settings := TRepoSettings.Create;
713+
// Marquee-Animation waehrend der Single-File-Analyse - analog zum
714+
// IDE-Plugin RunCurrent. Kein File-Phase-Callback (eine Datei).
715+
BeginAnalysisUI(0);
569716
try
570717
try Settings.Load; except end;
571718
ApplyDetectorConfig(Settings, True);
@@ -599,7 +746,7 @@ procedure TForm2.AnalyseSingleFile(const AFilePath: string);
599746
try Settings.PersistDiscoveredClasses; except end;
600747
finally
601748
Settings.Free;
602-
Screen.Cursor := crDefault;
749+
EndAnalysisUI;
603750
end;
604751
end;
605752

@@ -1034,7 +1181,6 @@ procedure TForm2.BtnBranchClick(Sender: TObject);
10341181
Exit;
10351182
end;
10361183

1037-
Screen.Cursor := crHourglass;
10381184
Settings := TRepoSettings.Create;
10391185
Files := nil;
10401186
Findings := nil;
@@ -1051,21 +1197,28 @@ procedure TForm2.BtnBranchClick(Sender: TObject);
10511197

10521198
StatusBar1.Panels[2].Text := Format(_('Analysing %d changed file(s). %s'),
10531199
[Files.Count, Info]);
1200+
// Total ist hier vorab bekannt -> direkt File-Phase (kein Marquee).
1201+
BeginAnalysisUI(Files.Count);
10541202
Application.ProcessMessages;
10551203

10561204
try
1057-
Findings := TStaticAnalyzer2.AnalyzeLeaksFromList(Files, nil,
1058-
Settings.UsesCheck);
1059-
FillGridFromFindings(Findings, StartDir);
1060-
except
1061-
on E: Exception do
1062-
StatusBar1.Panels[2].Text := _('Analysis error: ') + E.Message;
1205+
try
1206+
Findings := TStaticAnalyzer2.AnalyzeLeaksFromList(Files,
1207+
procedure(C, T: Integer) begin ProgressCallback(C, T); end,
1208+
Settings.UsesCheck);
1209+
FillGridFromFindings(Findings, StartDir);
1210+
except
1211+
on EAbort do ;
1212+
on E: Exception do
1213+
StatusBar1.Panels[2].Text := _('Analysis error: ') + E.Message;
1214+
end;
1215+
finally
1216+
EndAnalysisUI;
10631217
end;
10641218
finally
10651219
Findings.Free;
10661220
Files.Free;
10671221
Settings.Free;
1068-
Screen.Cursor := crDefault;
10691222
end;
10701223
end;
10711224

0 commit comments

Comments
 (0)