1 {
2  /***************************************************************************
3                               diffdialog.pas
4                               --------------
5 
6 
7  ***************************************************************************/
8 
9  ***************************************************************************
10  *                                                                         *
11  *   This source is free software; you can redistribute it and/or modify   *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  *   This code is distributed in the hope that it will be useful, but      *
17  *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
18  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU     *
19  *   General Public License for more details.                              *
20  *                                                                         *
21  *   A copy of the GNU General Public License is available on the World    *
22  *   Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also      *
23  *   obtain it by writing to the Free Software Foundation,                 *
24  *   Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA.   *
25  *                                                                         *
26  ***************************************************************************
27 
28   Author: Mattias Gaertner
29 
30   Abstract:
31     The TDiffDlg is a dialog for showing differences between two files.
32 
33 }
34 
35 unit DiffDialog;
36 
37 {$mode objfpc}{$H+}
38 
39 interface
40 
41 uses
42   Classes, SysUtils,
43   // LCL
44   Forms, Controls, Buttons, StdCtrls, ExtCtrls, Dialogs, ComCtrls, LCLType,
45   // LazUtils
46   FileUtil, lazutf8classes,
47   // SynEdit
48   SynEdit, SynHighlighterDiff,
49   // IdeIntf
50   IDEWindowIntf, IDEHelpIntf, IDEImagesIntf,
51   // IDE
52   LazarusIDEStrConsts, EditorOptions, InputHistory, DiffPatch, SourceEditor;
53 
54 type
55 
56   { TAvailableDiffFile }
57 
58   TAvailableDiffFile = class
59   private
60     Name: string;
61     Editor: TSourceEditor;
62     SelectionAvailable: boolean;
63   public
64     constructor Create(const NewName: string; NewEditor: TSourceEditor;
65       NewSelectionAvailable: boolean);
66   end;
67 
68   { TAvailableDiffFiles }
69 
70   TAvailableDiffFiles = class(TList)
71   private
GetItemsnull72     function GetItems(Index: integer): TAvailableDiffFile;
73     procedure SetItems(Index: integer; const AValue: TAvailableDiffFile);
74   public
75     procedure Clear; override;
Addnull76     function Add(DiffFile: TAvailableDiffFile): integer;
IndexOfNamenull77     function IndexOfName(const Name: string): integer;
78   public
79     property Items[Index: integer]: TAvailableDiffFile read GetItems write SetItems; default;
80   end;
81 
82   TDiffDlg = class;
83 
84   { TSelectedDiffFile }
85 
86   TSelectedDiffFile = class
87   private
88     fOwner: TDiffDlg;
89     fFile: TAvailableDiffFile;        // Selected File
90     fCombobox: TComboBox;             // Reference for the user selection GUI.
91     fOnlySelCheckBox: TCheckBox;
TextContentsnull92     function TextContents: string;
93     procedure SetIndex(NewIndex: integer);
94     procedure SetFileName(aFileName: string);
95     procedure UpdateIndex;
96   public
97     constructor Create(aOwner: TDiffDlg; aCombobox: TComboBox; aOnlySelCheckBox: TCheckBox);
98   end;
99 
100   { TDiffDlg }
101 
102   TDiffDlg = class(TForm)
103     HelpButton: TBitBtn;
104     CloseButton: TBitBtn;
105     DiffSynEdit: TSynEdit;
106     OpenInEditorButton: TBitBtn;
107     ProgressBar1: TProgressBar;
108     SynDiffSyn1: TSynDiffSyn;
109     Text1FileOpenButton: TButton;
110     CancelScanningButton: TBitBtn;
111     dlgOpen: TOpenDialog;
112 
113     Text2FileOpenButton: TButton;
114 
115     // text 1
116     Text1GroupBox: TGroupBox;
117     Text1Combobox: TComboBox;
118     Text1OnlySelectionCheckBox: TCheckBox;
119 
120     // text 2
121     Text2GroupBox: TGroupBox;
122     Text2Combobox: TComboBox;
123     Text2OnlySelectionCheckBox: TCheckBox;
124 
125     // options
126     OptionsGroupBox: TCheckGroup;
127 
128     procedure CancelScanningButtonClick(Sender: TObject);
129     procedure FileOpenClick(Sender: TObject);
130     procedure HelpButtonClick(Sender: TObject);
131     procedure OnChangeFlag(Sender: TObject);
132     procedure Text1ComboboxChange(Sender: TObject);
133     procedure Text2ComboboxChange(Sender: TObject);
134   private
135     fUpdating: Boolean;
136     fIdleConnected: boolean;
137     fCancelled: boolean;
138     fAvailableFiles: TAvailableDiffFiles;
139     fSelectedFile1: TSelectedDiffFile;
140     fSelectedFile2: TSelectedDiffFile;
141     procedure SetupComponents;
142     procedure UpdateDiff;
143     procedure SetIdleConnected(const AValue: boolean);
144     procedure OnIdle(Sender: TObject; var {%H-}Done: Boolean);
145     procedure UpdateProgress(aPosition: Integer);
146   public
147     constructor Create(TheOwner: TComponent); override;
148     destructor Destroy; override;
149     procedure Init;
150     procedure FillTextComboBoxes;
151     procedure SaveSettings;
152     procedure SetDiffOptions(NewOptions: TTextDiffFlags);
GetDiffOptionsnull153     function GetDiffOptions: TTextDiffFlags;
154 
155     property IdleConnected: boolean read fIdleConnected write SetIdleConnected;
156   end;
157 
ShowDiffDialognull158 function ShowDiffDialog(Text1Index: integer; out Diff: string): TModalResult;
159 
160 const
161   IgnoreCaseCheckBox = 0;
162   IgnoreEmptyLineChangesCheckBox = 1;
163   IgnoreHeadingSpacesCheckBox = 2;
164   IgnoreLineEndsCheckBox = 3;
165   IgnoreSpaceCharAmountCheckBox = 4;
166   IgnoreSpaceCharsCheckBox = 5;
167   IgnoreTrailingSpacesCheckBox = 6;
168 
169 implementation
170 
171 {$R *.lfm}
172 
ShowDiffDialognull173 function ShowDiffDialog(Text1Index: integer; out Diff: string): TModalResult;
174 var
175   DiffDlg: TDiffDlg;
176   Files: TAvailableDiffFiles;
177   i: Integer;
178   SrcEdit: TSourceEditor;
179 begin
180   DiffDlg := TDiffDlg.Create(nil);
181   Files := TAvailableDiffFiles.Create;
182   try
183     // Get available files
184     for i:=0 to SourceEditorManager.SourceEditorCount - 1 do begin
185       SrcEdit := SourceEditorManager.SourceEditors[i]; // FindSourceEditorWithPageIndex(i);
186       Files.Add(TAvailableDiffFile.Create(SrcEdit.PageName, SrcEdit, SrcEdit.SelectionAvailable));
187     end;
188     DiffDlg.fAvailableFiles := Files;
189     DiffDlg.fSelectedFile1.SetIndex(Text1Index);
190     DiffDlg.Init;
191     Result := DiffDlg.ShowModal;
192     DiffDlg.SaveSettings;
193     // "Open in editor" button returns mrYes.
194     if Result=mrYes then
195       Diff := DiffDlg.DiffSynEdit.Text;
196   finally
197     Files.Free;
198     DiffDlg.Free;
199   end;
200 end;
201 
202 { TSelectedDiffFile }
203 
204 constructor TSelectedDiffFile.Create(aOwner: TDiffDlg; aCombobox: TComboBox;
205   aOnlySelCheckBox: TCheckBox);
206 begin
207   inherited Create;
208   fOwner := aOwner;
209   fCombobox := aCombobox;
210   fOnlySelCheckBox := aOnlySelCheckBox;
211 end;
212 
TextContentsnull213 function TSelectedDiffFile.TextContents: string;
214 var
215   dat: TStringListUTF8;
216 begin
217   if fFile = nil then Exit('');
218   if fFile.Editor = nil then
219   begin
220     dat := TStringListUTF8.Create;
221     try
222       dat.LoadFromFile(fFile.Name);
223       Result := dat.Text;
224     finally
225       dat.Free;
226     end;
227   end
228   else begin
229     if (fFile.SelectionAvailable and fOnlySelCheckBox.Checked) then
230       Result := fFile.Editor.EditorComponent.SelText
231     else
232       Result := fFile.Editor.EditorComponent.Lines.Text;
233   end;
234 end;
235 
236 procedure TSelectedDiffFile.SetIndex(NewIndex: integer);
237 var
238   OldFile: TAvailableDiffFile;
239 begin
240   OldFile:=fFile;
241   if (NewIndex>=0) and (NewIndex<fOwner.fAvailableFiles.Count) then begin
242     fFile:=fOwner.fAvailableFiles[NewIndex];
243     fCombobox.Text:=fFile.Name;
244     fOnlySelCheckBox.Enabled:=fFile.SelectionAvailable;
245   end else begin
246     fFile:=nil;
247     fCombobox.Text:='';
248     fOnlySelCheckBox.Enabled:=false;
249   end;
250   if fFile<>OldFile then fOwner.UpdateDiff;
251 end;
252 
253 procedure TSelectedDiffFile.SetFileName(aFileName: string);
254 // Assumes that aFileName is already in fCombobox.Items.
255 begin
256   fCombobox.ItemIndex := fCombobox.Items.IndexOf(aFileName);
257   SetIndex(fCombobox.Items.IndexOf(aFileName));
258 end;
259 
260 procedure TSelectedDiffFile.UpdateIndex;
261 begin
262   SetIndex(fCombobox.Items.IndexOf(fCombobox.Text));
263 end;
264 
265 { TDiffDlg }
266 
267 constructor TDiffDlg.Create(TheOwner: TComponent);
268 begin
269   inherited Create(TheOwner);
270   fUpdating := False;
271   fCancelled := False;
272   fSelectedFile1 := TSelectedDiffFile.Create(Self, Text1Combobox, Text1OnlySelectionCheckBox);
273   fSelectedFile2 := TSelectedDiffFile.Create(Self, Text2Combobox, Text2OnlySelectionCheckBox);
274   Caption := lisCaptionCompareFiles;
275   IDEDialogLayoutList.ApplyLayout(Self,600,500);
276   SetupComponents;
277 end;
278 
279 destructor TDiffDlg.Destroy;
280 begin
281   fSelectedFile2.Free;
282   fSelectedFile1.Free;
283   inherited Destroy;
284 end;
285 
286 procedure TDiffDlg.OnChangeFlag(Sender: TObject);
287 begin
288   UpdateDiff;
289 end;
290 
291 procedure TDiffDlg.FileOpenClick(Sender: TObject);
292 begin
293   if dlgOpen.Execute then
294   begin
295     //only add new files
296     if Text1ComboBox.Items.IndexOf(dlgOpen.FileName) = -1 then
297     begin
298       fAvailableFiles.Add(TAvailableDiffFile.Create(dlgOpen.FileName,nil,False));
299       Text1ComboBox.Items.Add(dlgOpen.FileName);
300       Text2ComboBox.Items.Add(dlgOpen.FileName);
301     end;
302     //set the combobox and make the diff
303     if TButton(Sender) = Text1FileOpenButton then
304       fSelectedFile1.SetFileName(dlgOpen.FileName)
305     else
306       fSelectedFile2.SetFileName(dlgOpen.FileName);
307   end;
308 end;
309 
310 procedure TDiffDlg.CancelScanningButtonClick(Sender: TObject);
311 begin
312   fCancelled := True;
313 end;
314 
315 procedure TDiffDlg.HelpButtonClick(Sender: TObject);
316 begin
317   LazarusHelp.ShowHelpForIDEControl(Self);
318 end;
319 
320 procedure TDiffDlg.Text1ComboboxChange(Sender: TObject);
321 begin
322   fSelectedFile1.UpdateIndex;
323 end;
324 
325 procedure TDiffDlg.Text2ComboboxChange(Sender: TObject);
326 begin
327   fSelectedFile2.UpdateIndex;
328 end;
329 
330 procedure TDiffDlg.SetupComponents;
331 begin
332   // text 1
333   Text1GroupBox.Caption:=lisDiffDlgFile1;
334   Text1OnlySelectionCheckBox.Caption:=lisDiffDlgOnlySelection;
335   Text1FileOpenButton.Caption:='...';
336 
337   // text 2
338   Text2GroupBox.Caption:=lisDiffDlgFile2;
339   Text2OnlySelectionCheckBox.Caption:=lisDiffDlgOnlySelection;
340   Text2FileOpenButton.Caption:='...';
341 
342   // options
343   with OptionsGroupBox do
344   begin
345     Caption:=lisOptions;
346     Items.Add(lisDiffDlgCaseInsensitive);
347     Items.Add(lisDiffDlgIgnoreIfEmptyLinesWereAdd);
348     Items.Add(lisDiffDlgIgnoreSpacesAtStartOfLine);
349     Items.Add(lisDiffDlgIgnoreSpacesAtEndOfLine);
350     Items.Add(lisDiffDlgIgnoreIfLineEndCharsDiffe);
351     Items.Add(lisDiffDlgIgnoreIfSpaceCharsWereAdd);
352     Items.Add(lisDiffDlgIgnoreSpaces);
353   end;
354 
355   // buttons
356   IDEImages.AssignImage(CancelScanningButton, 'btn_cancel');
357   CloseButton.Caption:=lisClose;
358   OpenInEditorButton.Caption:=lisDiffDlgOpenDiffInEditor;
359   HelpButton.Caption:=lisMenuHelp;
360 
361   OpenInEditorButton.LoadGlyphFromStock(idButtonOpen);
362   if OpenInEditorButton.Glyph.Empty then
363     IDEImages.AssignImage(OpenInEditorButton, 'laz_open');
364 
365   // dialogs
366   dlgOpen.Title:=lisOpenExistingFile;
367   dlgOpen.Filter:=dlgFilterAll+' ('+GetAllFilesMask+')|'+GetAllFilesMask
368                  +'|'+dlgFilterLazarusUnit+' (*.pas;*.pp)|*.pas;*.pp'
369                  +'|'+dlgFilterLazarusProject+' (*.lpi)|*.lpi'
370                  +'|'+dlgFilterLazarusForm+' (*.lfm;*.dfm)|*.lfm;*.dfm'
371                  +'|'+dlgFilterLazarusPackage+' (*.lpk)|*.lpk'
372                  +'|'+dlgFilterLazarusProjectSource+' (*.lpr)|*.lpr';
373 
374   // diff
375   EditorOpts.GetSynEditSettings(DiffSynEdit);
376 end;
377 
378 procedure TDiffDlg.UpdateDiff;
379 begin
380   IdleConnected:=True;
381 end;
382 
383 procedure TDiffDlg.UpdateProgress(aPosition: Integer);
384 begin
385   ProgressBar1.Position := aPosition;
386   Application.ProcessMessages;
387 end;
388 
389 procedure TDiffDlg.SetIdleConnected(const AValue: boolean);
390 begin
391   if fIdleConnected=AValue then exit;
392   fIdleConnected:=AValue;
393   if fIdleConnected then
394     Application.AddOnIdleHandler(@OnIdle)
395   else
396     Application.RemoveOnIdleHandler(@OnIdle);
397 end;
398 
399 procedure TDiffDlg.OnIdle(Sender: TObject; var Done: Boolean);
400 var
401   Text1Src, Text2Src: string;
402   DiffOutput: TDiffOutput;
403 begin
404   IdleConnected := false;
405   if fUpdating then Exit;
406   fUpdating := True;
407   DiffSynEdit.Lines.Text := '';
408   Text1Src := fSelectedFile1.TextContents;
409   Text2Src := fSelectedFile2.TextContents;
410   if (Text1Src <> '') and (Text2Src <> '') then
411   begin
412     Text1GroupBox.Enabled := False;
413     Text2GroupBox.Enabled := False;
414     OpenInEditorButton.Enabled := False;
415     //CancelScanningButton.Enabled := True;
416 
417     DiffOutput := TDiffOutput.Create(Text1Src, Text2Src, GetDiffOptions);
418     try
419       ProgressBar1.Max := DiffOutput.GetProgressMax;
420       DiffOutput.OnProgressPos := @UpdateProgress;
421       DiffSynEdit.Lines.Text := DiffOutput.CreateTextDiff;
422     finally
423       DiffOutput.Free;
424     end;
425 
426     //CancelScanningButton.Enabled := False;
427     OpenInEditorButton.Enabled := True;
428     Text2GroupBox.Enabled := True;
429     Text1GroupBox.Enabled := True;
430   end;
431   fUpdating:=False;
432 end;
433 
434 procedure TDiffDlg.Init;
435 var
436   LastText2Name: String;
437   i: Integer;
438 begin
439   // fill all diff file names
440   FillTextComboBoxes;
441 
442   // get recent Text 2
443   i:=0;
444   LastText2Name:=InputHistories.DiffText2;
445   if LastText2Name<>'' then
446     i:=fAvailableFiles.IndexOfName(LastText2Name);
447   if i<0 then i:=0;
448   if i=fAvailableFiles.IndexOf(fSelectedFile2.fFile) then inc(i);
449   fSelectedFile2.SetIndex(i);
450 
451   // set recent options
452   SetDiffOptions(InputHistories.DiffFlags);
453 
454   // and action ...
455   UpdateDiff;
456 end;
457 
458 procedure TDiffDlg.FillTextComboBoxes;
459 var
460   i: Integer;
461 begin
462   // Text 1
463   Text1Combobox.Items.BeginUpdate;
464   Text1Combobox.Items.Clear;
465   for i:=0 to fAvailableFiles.Count-1 do
466     Text1Combobox.Items.Add(fAvailableFiles[i].Name);
467   Text1Combobox.Items.EndUpdate;
468 
469   // Text 2
470   Text2Combobox.Items.BeginUpdate;
471   Text2Combobox.Items.Clear;
472   for i:=0 to fAvailableFiles.Count-1 do
473     Text2Combobox.Items.Add(fAvailableFiles[i].Name);
474   Text2Combobox.Items.EndUpdate;
475 end;
476 
477 procedure TDiffDlg.SaveSettings;
478 begin
479   InputHistories.DiffFlags:=GetDiffOptions;
480   if (fSelectedFile2<>nil) and (fSelectedFile2.fFile<>nil) then begin
481     InputHistories.DiffText2:=fSelectedFile2.fFile.Name;
482     InputHistories.DiffText2OnlySelection:=Text2OnlySelectionCheckBox.Checked;
483   end else begin
484     InputHistories.DiffText2:='';
485     InputHistories.DiffText2OnlySelection:=false;
486   end;
487   IDEDialogLayoutList.SaveLayout(Self);
488 end;
489 
490 procedure TDiffDlg.SetDiffOptions(NewOptions: TTextDiffFlags);
491 begin
492   OptionsGroupBox.Checked[IgnoreCaseCheckBox]:=tdfIgnoreCase in NewOptions;
493   OptionsGroupBox.Checked[IgnoreEmptyLineChangesCheckBox]:=tdfIgnoreEmptyLineChanges in NewOptions;
494   OptionsGroupBox.Checked[IgnoreHeadingSpacesCheckBox]:=tdfIgnoreHeadingSpaces in NewOptions;
495   OptionsGroupBox.Checked[IgnoreLineEndsCheckBox]:=tdfIgnoreLineEnds in NewOptions;
496   OptionsGroupBox.Checked[IgnoreSpaceCharAmountCheckBox]:=tdfIgnoreSpaceCharAmount in NewOptions;
497   OptionsGroupBox.Checked[IgnoreSpaceCharsCheckBox]:=tdfIgnoreSpaceChars in NewOptions;
498   OptionsGroupBox.Checked[IgnoreTrailingSpacesCheckBox]:=tdfIgnoreTrailingSpaces in NewOptions;
499 end;
500 
TDiffDlg.GetDiffOptionsnull501 function TDiffDlg.GetDiffOptions: TTextDiffFlags;
502 begin
503   Result:=[];
504   if OptionsGroupBox.Checked[IgnoreCaseCheckBox] then
505     Include(Result,tdfIgnoreCase);
506   if OptionsGroupBox.Checked[IgnoreEmptyLineChangesCheckBox] then
507     Include(Result,tdfIgnoreEmptyLineChanges);
508   if OptionsGroupBox.Checked[IgnoreHeadingSpacesCheckBox] then
509     Include(Result,tdfIgnoreHeadingSpaces);
510   if OptionsGroupBox.Checked[IgnoreLineEndsCheckBox] then
511     Include(Result,tdfIgnoreLineEnds);
512   if OptionsGroupBox.Checked[IgnoreSpaceCharAmountCheckBox] then
513     Include(Result,tdfIgnoreSpaceCharAmount);
514   if OptionsGroupBox.Checked[IgnoreSpaceCharsCheckBox] then
515     Include(Result,tdfIgnoreSpaceChars);
516   if OptionsGroupBox.Checked[IgnoreTrailingSpacesCheckBox] then
517     Include(Result,tdfIgnoreTrailingSpaces);
518 end;
519 
520 { TAvailableDiffFile }
521 
522 constructor TAvailableDiffFile.Create(const NewName: string; NewEditor: TSourceEditor;
523   NewSelectionAvailable: boolean);
524 begin
525   Name:=NewName;
526   Editor:=NewEditor;
527   SelectionAvailable:=NewSelectionAvailable;
528 end;
529 
530 { TAvailableDiffFiles }
531 
TAvailableDiffFiles.GetItemsnull532 function TAvailableDiffFiles.GetItems(Index: integer): TAvailableDiffFile;
533 begin
534   Result:=TAvailableDiffFile(inherited Items[Index]);
535 end;
536 
537 procedure TAvailableDiffFiles.SetItems(Index: integer; const AValue: TAvailableDiffFile);
538 begin
539   inherited Items[Index]:=AValue;
540 end;
541 
542 procedure TAvailableDiffFiles.Clear;
543 var
544   i: Integer;
545 begin
546   for i:=0 to Count-1 do
547     Items[i].Free;
548   inherited Clear;
549 end;
550 
Addnull551 function TAvailableDiffFiles.Add(DiffFile: TAvailableDiffFile): integer;
552 begin
553   Result:=inherited Add(DiffFile);
554 end;
555 
IndexOfNamenull556 function TAvailableDiffFiles.IndexOfName(const Name: string): integer;
557 begin
558   Result:=Count-1;
559   while (Result>=0) and (Items[Result].Name<>Name) do dec(Result);
560 end;
561 
562 end.
563 
564