@@ -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;
299337end ;
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+
301435procedure TForm2.FormResizeHandler (Sender: TObject);
302436begin
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>;
517651begin
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 ;
548693end ;
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 ;
604751end ;
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 ;
10701223end ;
10711224
0 commit comments