1 unit EditorMacroListViewer;
2 
3 {$mode objfpc}{$H+}
4 
5 interface
6 
7 uses
8   Classes, SysUtils,
9   // LCL
10   LCLType, Forms, Controls, Dialogs, StdCtrls, ButtonPanel, ComCtrls, ExtCtrls,
11   Spin, Menus, Buttons,
12   // LazUtils
13   LazFileUtils, LazStringUtils, Laz2_XMLCfg, LazUTF8, LazLoggerBase,
14   // SynEdit
15   SynMacroRecorder, SynEdit, SynEditKeyCmds,
16   // IdeIntf
17   IDEWindowIntf, IDEImagesIntf, SrcEditorIntf, IDEHelpIntf, IDECommands,
18   LazIDEIntf, IDEDialogs,
19   // IDE
20   LazarusIDEStrConsts, ProjectDefs, LazConf, Project, KeyMapping,
21   KeyMapShortCutDlg, MainIntf;
22 
23 type
24   TSynEditorMacro = class(TSynMacroRecorder)
25   end;
26 
27   { TIdeEditorMacro }
28 
29   TIdeEditorMacro = class(TEditorMacro)
30   private
31     FMacroName: String;
32     FHasError: Boolean;
33     FErrorMsg: String;
34     FFailedText: String;
35     FSynMacro: TSynEditorMacro;
36     FKeyBinding: TEditorMacroKeyBinding;
37 
38     procedure DoMacroRecorderState(Sender: TObject);
39     procedure DoMacroRecorderUserCommand({%H-}aSender: TCustomSynMacroRecorder;
40                 aCmd: TSynEditorCommand; var aEvent: TSynMacroEvent);
41   protected
GetMacroNamenull42     function  GetMacroName: String; override;
43     procedure SetMacroName(AValue: string); override;
GetStatenull44     function  GetState: TEditorMacroState; override;
GetErrorMsgnull45     function  GetErrorMsg: String; override;
GetKeyBindingnull46     function  GetKeyBinding: TEditorMacroKeyBinding; override;
47 
48     procedure DoRecordMacro(aEditor: TWinControl); override;
49     procedure DoPlaybackMacro(aEditor: TWinControl); override;
50     procedure DoStop; override;
51     procedure DoPause; override;
52     procedure DoResume; override;
53   public
54     constructor Create(aOwner: TComponent); override;
55     destructor Destroy; override;
56     procedure AssignEventsFrom(AMacroRecorder: TEditorMacro); override;
AddEditornull57     function  AddEditor(AValue: TCustomSynEdit): integer;
58 
59     procedure Clear; override;
60 
GetAsSourcenull61     function  GetAsSource: String; override;
62     procedure SetFromSource(const AText: String); override;
63     procedure WriteToXmlConf(AConf: TXMLConfig; const APath: String); override;
64     procedure ReadFromXmlConf(AConf: TXMLConfig; const APath: String); override;
65 
IsEmptynull66     function  IsEmpty: Boolean; override;
IsInvalidnull67     function  IsInvalid: Boolean; override;
IsRecordingnull68     function  IsRecording(AnEditor: TWinControl): Boolean; override;
69   end;
70 
71 
72   { TIdeEditorMacroKeyBinding }
73 
74   TIdeEditorMacroKeyBinding = class(TEditorMacroKeyBinding)
75   protected
76     FIdeCmd: TIDECommand;
GetIdeCmdnull77     function  GetIdeCmd: TIDECommand; override;
78     procedure ExecMacro(Sender: TObject);
79   public
80     destructor Destroy; override;
81     procedure WriteToXmlConf(AConf: TXMLConfig; const APath: String); override;
82     procedure ReadFromXmlConf(AConf: TXMLConfig; const APath: String); override;
83     procedure MacroNameChanged; override;
ShortCutAsTextnull84     function  ShortCutAsText: String; override;
85   end;
86 
87   { TIdeMacroEventWriter }
88 
89   TIdeMacroEventWriter = class(TSynMacroEventWriter)
90   private
91     FText: String;
92     FCmdName, FParams: String;
93     FUseLineFeed: Boolean;
94   public
95     constructor Create;
96     procedure BeginEvent;
97     procedure FinishEvent;
98     procedure WriteEventCommand(const ACmd: TSynEditorCommand); override;
99     procedure WriteEventParam(const AParam: string); override;
100     procedure WriteEventParam(const AParam: integer); override;
101     property Text: String read FText;
102     property UseLineFeed: Boolean read FUseLineFeed write FUseLineFeed;
103   end;
104 
105   { TIdeMacroEventReader }
106 
107   TIdeMacroEventReader = class(TSynMacroEventReader)
108   private
109     FErrorText: String;
110     FEventName: String;
111     FHasError: Boolean;
112     FText, FOrigText: String;
113     FPos, FPosCompensate: Integer;
114     FEventCommand: TSynEditorCommand;
115     FParams: Array of record
116         ParamType: TSynEventParamType;
117         Text : String;
118         Num: Integer;
119       end;
120   protected
GetParamAsIntnull121     function GetParamAsInt(Index: Integer): Integer; override;
GetParamAsStringnull122     function GetParamAsString(Index: Integer): String; override;
GetParamTypenull123     function GetParamType(Index: Integer): TSynEventParamType; override;
PosToXYnull124     function PosToXY: TPoint;
AddErrornull125     function AddError(AMsg: string): Boolean;
126   public
127     constructor Create(const Atext: String);
EventCommandnull128     function  EventCommand: TSynEditorCommand; override;
ParamCountnull129     function  ParamCount: Integer; override;
ParseNextEventnull130     function  ParseNextEvent: Boolean;
131     property  EventName: String read FEventName;
132     property  HasError: Boolean read FHasError;
133     property  ErrorText: String read FErrorText;
134   end;
135 
136   { TEditorMacroList }
137 
138   TEditorMacroList = class;
139   TMacroAddedEvent = procedure(AList: TEditorMacroList; AMacro: TEditorMacro) of object;
140 
141   TEditorMacroList = class
142   private
143     FList: TList;
144     FOnAdded: TMacroAddedEvent;
145     FOnChange: TNotifyEvent;
146     FOnRemove: TMacroAddedEvent;
GetMacrosnull147     function GetMacros(Index: Integer): TEditorMacro;
148     procedure DoChanged;
149     procedure DoAdded(AMacro: TEditorMacro);
150     procedure DoRemove(AMacro: TEditorMacro);
151   public
152     constructor Create;
153     destructor Destroy; override;
154     procedure WriteToXmlConf(AConf: TXMLConfig; const APath: String);
155     procedure ReadFromXmlConf(AConf: TXMLConfig; const APath: String);
156     procedure ClearAndFreeMacros;
Countnull157     function Count: Integer;
IndexOfnull158     function IndexOf(AMacro: TEditorMacro): Integer;
IndexOfNamenull159     function IndexOfName(AName: String): Integer;
UniqNamenull160     function UniqName(AName: String): String;
Addnull161     function Add(AMacro: TEditorMacro): Integer;
162     procedure Delete(AnIndex: Integer);
163     procedure Remove(AMacro: TEditorMacro);
164     property Macros[Index: Integer]: TEditorMacro read GetMacros;
165     property OnChange: TNotifyEvent read FOnChange write FOnChange;
166     property OnAdded: TMacroAddedEvent read FOnAdded write FOnAdded;
167     property OnRemove: TMacroAddedEvent read FOnRemove write FOnRemove;
168   end;
169 
170   { TMacroListView }
171 
172   TMacroListView = class(TForm)
173     btnEdit: TButton;
174     btnSetKeys: TButton;
175     btnPlay: TButton;
176     btnRecord: TButton;
177     btnRecordStop: TButton;
178     btnDelete: TButton;
179     btnSelect: TButton;
180     btnRename: TButton;
181     ButtonPanel1: TButtonPanel;
182     chkRepeat: TCheckBox;
183     GroupBox1: TGroupBox;
184     LabelWarning: TLabel;
185     lbMoveTo: TLabel;
186     lbMacroView: TListView;
187     mnExport: TMenuItem;
188     mnImport: TMenuItem;
189     OpenDialog1: TOpenDialog;
190     Panel1: TPanel;
191     PnlWarnClose: TPanel;
192     PanelWarnings: TPanel;
193     PanelRepeat: TPanel;
194     pnlButtons: TPanel;
195     PopupMenu1: TPopupMenu;
196     RenameButton: TPanelBitBtn;
197     edRepeat: TSpinEdit;
198     SaveDialog1: TSaveDialog;
199     BtnWarnClose: TSpeedButton;
200     ToolBar1: TToolBar;
201     tbRecorded: TToolButton;
202     tbProject: TToolButton;
203     tbIDE: TToolButton;
204     ToolBar2: TToolBar;
205     tbMoveProject: TToolButton;
206     tbMoveIDE: TToolButton;
207     ToolButton3: TToolButton;
208     ToolButton4: TToolButton;
209     procedure btnDeleteClick(Sender: TObject);
210     procedure btnEditClick(Sender: TObject);
211     procedure btnPlayClick(Sender: TObject);
212     procedure btnRecordClick(Sender: TObject);
213     procedure btnRecordStopClick(Sender: TObject);
214     procedure btnRenameClick(Sender: TObject);
215     procedure btnSelectClick(Sender: TObject);
216     procedure btnSetKeysClick(Sender: TObject);
217     procedure BtnWarnCloseClick(Sender: TObject);
218     procedure FormActivate(Sender: TObject);
219     procedure HelpButtonClick(Sender: TObject);
220     procedure lbMacroViewSelectItem(Sender: TObject; {%H-}Item: TListItem; {%H-}Selected: Boolean);
221     procedure mnExportClick(Sender: TObject);
222     procedure mnImportClick(Sender: TObject);
223     procedure tbIDEClick(Sender: TObject);
224     procedure tbMoveIDEClick(Sender: TObject);
225     procedure tbMoveProjectClick(Sender: TObject);
226     procedure tbProjectClick(Sender: TObject);
227     procedure tbRecordedClick(Sender: TObject);
228   private
229     FImageRec: Integer;
230     FImagePlay: Integer;
231     FImageSel: Integer;
232     FImageErr: Integer;
233     FIsPlaying: Boolean;
234     FIgnoreMacroChanges: Boolean;
235     procedure DoOnMacroListChange(Sender: TObject);
236     procedure DoMacroContentChanged(Sender: TObject);
237     procedure DoMacroStateChanged(Sender: TObject);
238     procedure UpdateButtons;
239   protected
240     procedure DoEditorMacroStateChanged;
241   public
242     constructor Create(TheOwner: TComponent); override;
243     destructor Destroy; override;
MacroByFullNamenull244     function  MacroByFullName(AName: String): TEditorMacro;
245     procedure UpdateDisplay;
246   end;
247 
MacroListViewernull248 function MacroListViewer: TMacroListView;
249 procedure ShowMacroListViewer;
250 procedure UpdateMacroListViewer;
251 procedure DoEditorMacroStateChanged;
252 
253 procedure LoadProjectSpecificInfo(XMLConfig: TXMLConfig);
254 procedure SaveProjectSpecificInfo(XMLConfig: TXMLConfig; Flags: TProjectWriteFlags);
255 procedure LoadGlobalInfo;
256 procedure SaveGlobalInfo;
257 
258 var
259   OnKeyMapReloaded: procedure of object;
260   OnEditorMacroStateChange: TNotifyEvent;
261   // SelectedEditorMacro: Selected, for playing with default shortcut
262   SelectedEditorMacro: TEditorMacro = nil;
263 
264 const
265   EditorMacroVirtualDrive = '%Macro:|'; // do not use \ or /, they can be converted by the IDE
266 
267 implementation
268 
269 var
270   MacroListView: TMacroListView = nil;
271 
272   CurrentEditorMacroList: TEditorMacroList = nil;
273   EditorMacroListRec, EditorMacroListProj, EditorMacroListGlob: TEditorMacroList;
274 
275   // CurrentRecordingMacro:
276   // The player-macro, to wich the recording in process will be assigned
277   CurrentRecordingMacro: TEditorMacro = nil;
278 
279   MacroRecCounter: Integer = 1;
280   KeyCategory: TIDECommandCategory = nil;
281 
282 const
283   GlobalConfFileName = 'EditorMacros.xml';
284 
MacroListViewernull285 function MacroListViewer: TMacroListView;
286 begin
287   if MacroListView = nil then
288     MacroListView := TMacroListView.Create(Application);
289   Result := MacroListView;
290 
291   MacroListView.LabelWarning.Caption := MacroListViewerWarningText;
292   MacroListView.PanelWarnings.Visible := MacroListViewerWarningText <> '';
293 end;
294 
295 procedure DoMacroListViewerWarningChanged({%H-}ASender: TObject);
296 begin
297   if MacroListView <> nil then begin
298     MacroListView.LabelWarning.Caption := MacroListViewerWarningText;
299     MacroListView.PanelWarnings.Visible := MacroListViewerWarningText <> '';
300   end;
301 end;
302 
303 procedure ShowMacroListViewer;
304 begin
305   IDEWindowCreators.ShowForm(MacroListViewer, True);
306 end;
307 
308 procedure UpdateMacroListViewer;
309 begin
310   if MacroListView <> nil then
311     MacroListView.UpdateDisplay;
312 end;
313 
MacroListToNamenull314 function MacroListToName(AList: TEditorMacroList): string;
315 begin
316   Result := '';
317   if AList = EditorMacroListRec then Result := 'Rec'
318   else if AList = EditorMacroListProj then Result := 'Prj'
319   else if AList = EditorMacroListGlob then Result := 'Ide';
320 end;
321 
NameToMacroListnull322 function NameToMacroList(AName: string): TEditorMacroList;
323 begin
324   Result := nil;
325   if AName = 'Rec' then Result := EditorMacroListRec
326   else if AName = 'Prj' then Result := EditorMacroListProj
327   else if AName = 'Ide' then Result := EditorMacroListGlob;
328 end;
329 
330 procedure DoEditorMacroStateChanged;
331 begin
332   if EditorMacroForRecording= nil then exit;
333 
334   if not(EditorMacroForRecording.State in [emRecording, emRecPaused]) and
335     (CurrentRecordingMacro <> nil)
336   then begin
337     // finished recording
338     if EditorMacroForRecording.IsEmpty then begin
339       EditorMacroListRec.Remove(CurrentRecordingMacro);
340       FreeAndNil(CurrentRecordingMacro);
341     end else begin
342       CurrentRecordingMacro.AssignEventsFrom(EditorMacroForRecording);
343       SelectedEditorMacro := CurrentRecordingMacro;
344       CurrentRecordingMacro := nil;
345     end;
346   end;
347 
348   if (EditorMacroForRecording.State = emRecording) and (CurrentRecordingMacro = nil) then begin
349     CurrentRecordingMacro := EditorMacroPlayerClass.Create(nil);
350     CurrentRecordingMacro.OnStateChange := @MacroListViewer.DoMacroStateChanged;
351     CurrentRecordingMacro.OnChange := @MacroListViewer.DoMacroContentChanged;
352     CurrentRecordingMacro.MacroName := Format(lisNewMacroName, [MacroRecCounter]);
353     inc(MacroRecCounter);
354     EditorMacroListRec.Add(CurrentRecordingMacro);
355   end;
356 
357   if MacroListView <> nil then
358     MacroListView.DoEditorMacroStateChanged;
359 end;
360 
361 procedure LoadProjectSpecificInfo(XMLConfig: TXMLConfig);
362 begin
363   MacroListViewer.FIgnoreMacroChanges := True;
364   try
365     EditorMacroListProj.ReadFromXmlConf(XMLConfig, '');
366   finally
367     MacroListViewer.FIgnoreMacroChanges := False;
368   end;
369 end;
370 
371 procedure SaveProjectSpecificInfo(XMLConfig: TXMLConfig; Flags: TProjectWriteFlags);
372 begin
373   if not (pwfSkipSeparateSessionInfo in Flags) then
374   begin
375     EditorMacroListProj.WriteToXmlConf(XMLConfig, '');
376   end;
377 end;
378 
379 procedure LoadGlobalInfo;
380 var
381   Filename: String;
382   XMLConfig: TXMLConfig;
383 begin
384   MacroListViewer.FIgnoreMacroChanges := True;
385   Filename := TrimFilename(AppendPathDelim(GetPrimaryConfigPath)+GlobalConfFileName);
386   try
387     XMLConfig := TXMLConfig.Create(Filename);
388     try
389       EditorMacroListGlob.ReadFromXmlConf(XMLConfig, '');
390     finally
391       XMLConfig.Free;
392     end;
393   except
394     on E: Exception do begin
395       DebugLn('[EditorMacroListViewer.LoadGlobalInfo]  error reading "',Filename,'": ',E.Message);
396     end;
397   end;
398   MacroListViewer.FIgnoreMacroChanges := False;
399 end;
400 
401 procedure SaveGlobalInfo;
402 var
403   Filename: String;
404   XMLConfig: TXMLConfig;
405 begin
406   Filename := TrimFilename(AppendPathDelim(GetPrimaryConfigPath)+GlobalConfFileName);
407   try
408     XMLConfig := TXMLConfig.CreateClean(Filename);
409     try
410       EditorMacroListGlob.WriteToXmlConf(XMLConfig, '');
411     finally
412       XMLConfig.Free;
413     end;
414   except
415     on E: Exception do begin
416       DebugLn('[EditorMacroListViewer.SaveGlobalInfo]  error writing "',Filename,'": ',E.Message);
417     end;
418   end;
419 end;
420 
421 { TIdeEditorMacroKeyBinding }
422 
GetIdeCmdnull423 function TIdeEditorMacroKeyBinding.GetIdeCmd: TIDECommand;
424 begin
425   Result := FIdeCmd;
426 end;
427 
428 procedure TIdeEditorMacroKeyBinding.ExecMacro(Sender: TObject);
429 begin
430   if ActiveEditorMacro <> nil then exit;
431   FOwner.PlaybackMacro(TCustomSynEdit(SourceEditorManagerIntf.ActiveEditor.EditorControl));
432 end;
433 
434 procedure TIdeEditorMacroKeyBinding.MacroNameChanged;
435 begin
436   if (IDECommandList = nil) then
437     exit;
438 
439   if FOwner.IsInvalid then begin
440     if FIdeCmd <> nil then begin
441       (IDECommandList as TKeyCommandRelationList).RemoveCommand(FIdeCmd);
442       FreeAndNil(FIdeCmd);
443     end;
444     exit;
445   end;
446 
447   if KeyCategory = nil then
448     KeyCategory := IDECommandList.CreateCategory(nil, 'EditorMacros',
449       'Editor Macros', IDECmdScopeSrcEditOnly);
450 
451   if FIdeCmd = nil then begin
452     FIdeCmd := (IDECommandList as TKeyCommandRelationList).CreateCommand(
453       KeyCategory,
454       'EdtMacro'+FOwner.MacroName,
455       FOwner.MacroName,
456       IDEShortCut(VK_UNKNOWN, [], VK_UNKNOWN, []),
457       IDEShortCut(VK_UNKNOWN, [], VK_UNKNOWN, []),
458       @ExecMacro, nil
459     );
460     (FIdeCmd as TKeyCommandRelation).SkipSaving := True;
461   end
462   else
463     FIdeCmd.LocalizedName := FOwner.MacroName;
464 
465 end;
466 
467 destructor TIdeEditorMacroKeyBinding.Destroy;
468 begin
469   inherited Destroy;
470   if (FIdeCmd <> nil) and (IDECommandList <> nil) then begin
471     (IDECommandList as TKeyCommandRelationList).RemoveCommand(FIdeCmd);
472     FreeAndNil(FIdeCmd);
473   end;
474 end;
475 
476 procedure TIdeEditorMacroKeyBinding.WriteToXmlConf(AConf: TXMLConfig; const APath: String);
477   procedure ClearKey(const SubPath: string);
478   begin
479     AConf.DeleteValue(SubPath+'Key1');
480     AConf.DeleteValue(SubPath+'Shift1');
481     AConf.DeleteValue(SubPath+'Key2');
482     AConf.DeleteValue(SubPath+'Shift2');
483   end;
484   procedure Store(const SubPath: string; Key: TIDEShortCut);
485   var
486     s: TShiftState;
487   begin
488     AConf.SetDeleteValue(SubPath+'Key1', key.Key1, VK_UNKNOWN);
489     if key.Key1=VK_UNKNOWN then
490       s:=[]
491     else
492       s:=key.Shift1;
493     AConf.SetDeleteValue(SubPath+'Shift1',ShiftStateToCfgStr(s),ShiftStateToCfgStr([]));
494     AConf.SetDeleteValue(SubPath+'Key2',key.Key2,VK_UNKNOWN);
495     if key.Key2=VK_UNKNOWN then
496       s:=[]
497     else
498       s:=key.Shift2;
499     AConf.SetDeleteValue(SubPath+'Shift2',ShiftStateToCfgStr(s),ShiftStateToCfgStr([]));
500   end;
501 
502 begin
503   if (FIdeCmd = nil) then begin
504     ClearKey(APath + 'KeyA/');
505     ClearKey(APath + 'KeyB/');
506   end else begin
507     Store(APath + 'KeyA/', FIdeCmd.ShortcutA);
508     Store(APath + 'KeyB/', FIdeCmd.ShortcutB);
509   end;
510 end;
511 
512 procedure TIdeEditorMacroKeyBinding.ReadFromXmlConf(AConf: TXMLConfig; const APath: String);
513   procedure Load(SubPath: string; out Key: TIDEShortCut);
514   begin
515     key.Key1   := AConf.GetValue(SubPath+'Key1',VK_UNKNOWN);
516     key.Shift1 := CfgStrToShiftState(AConf.GetValue(SubPath+'Shift1',''));
517     key.Key2   := AConf.GetValue(SubPath+'Key2',VK_UNKNOWN);
518     key.Shift2 := CfgStrToShiftState(AConf.GetValue(SubPath+'Shift2',''));
519   end;
520 var
521   SCut: TIDEShortCut;
522 begin
523   if (FIdeCmd <> nil) then begin
524     Load(APath+'KeyA/', SCut);
525     if (IDECommandList as TKeyCommandRelationList).Find(SCut, TSourceEditorWindowInterface) = nil then
526       FIdeCmd.ShortcutA := SCut;
527 
528     Load(APath+'KeyB/', SCut);
529     if (IDECommandList as TKeyCommandRelationList).Find(SCut, TSourceEditorWindowInterface) = nil then
530       FIdeCmd.ShortcutB := SCut;
531   end;
532 end;
533 
ShortCutAsTextnull534 function TIdeEditorMacroKeyBinding.ShortCutAsText: String;
535 begin
536   Result := '';
537   If FIdeCmd = nil then
538     exit;
539   if not IDEShortCutEmpty(FIdeCmd.ShortcutA) then
540     Result := Result + ' (' + KeyAndShiftStateToEditorKeyString(FIdeCmd.ShortcutA) + ')';
541   if not IDEShortCutEmpty(FIdeCmd.ShortcutB) then
542     Result := Result + ' (' + KeyAndShiftStateToEditorKeyString(FIdeCmd.ShortcutB) + ')';
543 end;
544 
545 { TIdeEditorMacro }
546 
GetMacroNamenull547 function TIdeEditorMacro.GetMacroName: String;
548 begin
549   Result := FMacroName;
550 end;
551 
552 procedure TIdeEditorMacro.DoMacroRecorderState(Sender: TObject);
553 begin
554   DoStateChanged;
555   CheckStateAndActivated;
556 end;
557 
558 procedure TIdeEditorMacro.DoMacroRecorderUserCommand(aSender: TCustomSynMacroRecorder;
559   aCmd: TSynEditorCommand; var aEvent: TSynMacroEvent);
560 begin
561   case aCmd of
562     ecSynMacroPlay, ecSynMacroRecord,
563     ecToggleFormUnit..ecViewThreads, ecViewHistory,
564     ecNextEditor, ecPrevEditor, ecNextWindow, ecPrevWindow,
565     ecPrevEditorInHistory, ecNextEditorInHistory,
566     ecGotoEditor1..ecGotoEditor0:
567       aEvent := TSynSkippedEvent.Create;
568     else
569       ;//
570   end;
571 end;
572 
GetStatenull573 function TIdeEditorMacro.GetState: TEditorMacroState;
574 begin
575   case FSynMacro.state of
576     msStopped:   Result := emStopped;
577     msRecording: Result := emRecording;
578     msPlaying:   Result := emPlaying;
579     msPaused:    Result := emRecPaused;
580   end;
581 end;
582 
GetErrorMsgnull583 function TIdeEditorMacro.GetErrorMsg: String;
584 begin
585   Result := FErrorMsg;
586 end;
587 
GetKeyBindingnull588 function TIdeEditorMacro.GetKeyBinding: TEditorMacroKeyBinding;
589 begin
590   if FKeyBinding = nil then
591     FKeyBinding := GetDefaultKeyBinding;
592   Result := FKeyBinding;
593 end;
594 
595 procedure TIdeEditorMacro.SetMacroName(AValue: string);
596 begin
597   FMacroName := AValue;
598   FSynMacro.MacroName := AValue;
599   DoChanged;
600 end;
601 
602 constructor TIdeEditorMacro.Create(aOwner: TComponent);
603 begin
604   FSynMacro := TSynEditorMacro.Create(aOwner);
605   FHasError := False;
606 
607   FSynMacro.OnUserCommand   := @DoMacroRecorderUserCommand;
608   FSynMacro.OnStateChange  := @DoMacroRecorderState;
609   FSynMacro.RecordCommandID := ecNone; // ecSynMacroRecord;
610   FSynMacro.PlaybackCommandID := ecNone; // ecSynMacroPlay;
611   FSynMacro.RecordShortCut := 0;
612   FSynMacro.PlaybackShortCut := 0;
613 end;
614 
615 destructor TIdeEditorMacro.Destroy;
616 begin
617   inherited Destroy;
618   FreeAndNil(FSynMacro);
619   FreeAndNil(FKeyBinding);
620 end;
621 
622 procedure TIdeEditorMacro.AssignEventsFrom(AMacroRecorder: TEditorMacro);
623 begin
624   if AMacroRecorder = nil then
625     Clear
626   else
627   if AMacroRecorder is TIdeEditorMacro then
628     FSynMacro.AssignEventsFrom(TIdeEditorMacro(AMacroRecorder).FSynMacro)
629   else
630     SetFromSource(AMacroRecorder.GetAsSource);
631 
632   DoChanged;
633 end;
634 
TIdeEditorMacro.AddEditornull635 function TIdeEditorMacro.AddEditor(AValue: TCustomSynEdit): integer;
636 begin
637   Result := FSynMacro.AddEditor(AValue);
638 end;
639 
640 procedure TIdeEditorMacro.DoRecordMacro(aEditor: TWinControl);
641 begin
642   FSynMacro.RecordMacro(aEditor as TCustomSynEdit);
643 end;
644 
645 procedure TIdeEditorMacro.DoPlaybackMacro(aEditor: TWinControl);
646 begin
647   FSynMacro.PlaybackMacro(aEditor as TCustomSynEdit);
648 end;
649 
650 procedure TIdeEditorMacro.DoStop;
651 begin
652   FSynMacro.Stop;
653 end;
654 
655 procedure TIdeEditorMacro.DoPause;
656 begin
657   FSynMacro.Pause;
658 end;
659 
660 procedure TIdeEditorMacro.DoResume;
661 begin
662   FSynMacro.Resume;
663 end;
664 
665 procedure TIdeEditorMacro.Clear;
666 begin
667   FSynMacro.Clear;
668 
669   DoChanged;
670 end;
671 
TIdeEditorMacro.GetAsSourcenull672 function TIdeEditorMacro.GetAsSource: String;
673 var
674   i : integer;
675   W: TIdeMacroEventWriter;
676 begin
677   if FHasError then begin
678     Result := FFailedText;
679     exit;
680   end;
681 
682   W := TIdeMacroEventWriter.Create;
683   W.UseLineFeed := True;
684   try
685     for i := 0 to FSynMacro.EventCount -1 do
686     begin
687       W.BeginEvent;
688       FSynMacro.Events[i].SaveToWriter(W);
689       W.FinishEvent;
690     end;
691     Result := w.Text;
692   finally
693     W.Free;
694   end;
695 end;
696 
697 procedure TIdeEditorMacro.SetFromSource(const AText: String);
698 var
699   iEvent: TSynMacroEvent;
700   R: TIdeMacroEventReader;
701 begin
702   Stop;
703   FSynMacro.Clear;
704   FHasError := False;
705 
706   R := TIdeMacroEventReader.Create(AText);
707   try
708     while R.ParseNextEvent do begin
709       iEvent := FSynMacro.CreateMacroEvent(R.EventCommand);
710       iEvent.LoadFromReader(R);
711       FSynMacro.InsertCustomEvent(FSynMacro.EventCount, iEvent);
712     end;
713     if R.HasError then begin
714       FHasError := True;
715       FErrorMsg := R.ErrorText;
716       FFailedText := AText;
717       MacroName := MacroName;
718     end;
719   finally
720     R.Free;
721   end;
722   DoChanged;
723 end;
724 
725 procedure TIdeEditorMacro.WriteToXmlConf(AConf: TXMLConfig; const APath: String);
726 begin
727   AConf.SetValue(APath + 'Name', MacroName);
728   AConf.SetValue(APath + 'Code/Value', GetAsSource);
729 
730   if (KeyBinding <> nil) then
731     KeyBinding.WriteToXmlConf(AConf, APath);
732 end;
733 
734 procedure TIdeEditorMacro.ReadFromXmlConf(AConf: TXMLConfig; const APath: String);
735 var
736   s: String;
737 begin
738   s := AConf.GetValue(APath + 'Code/Value', '');
739   SetFromSource(s);
740   s := AConf.GetValue(APath + 'Name', '');
741   if s <> '' then MacroName := s;
742 
743   if (not FHasError) and (FSynMacro.EventCount = 0) then begin
744     FHasError := True;
745     FErrorMsg := 'No content found';
746     FFailedText := s;
747   end;
748 
749   if (KeyBinding <> nil) then
750     KeyBinding.ReadFromXmlConf(AConf, APath);
751 
752   DoChanged;
753 end;
754 
IsEmptynull755 function TIdeEditorMacro.IsEmpty: Boolean;
756 begin
757   Result := FSynMacro.IsEmpty;
758 end;
759 
IsInvalidnull760 function TIdeEditorMacro.IsInvalid: Boolean;
761 begin
762   Result := FHasError;
763 end;
764 
TIdeEditorMacro.IsRecordingnull765 function TIdeEditorMacro.IsRecording(AnEditor: TWinControl): Boolean;
766 begin
767   Result := (State in [emRecording, emRecPaused]) and
768             (FSynMacro.CurrentEditor = AnEditor);
769 end;
770 
771 { TIdeMacroEventReader }
772 
TIdeMacroEventReader.GetParamAsIntnull773 function TIdeMacroEventReader.GetParamAsInt(Index: Integer): Integer;
774 begin
775   if (Index < 0) or (Index >= Length(FParams)) or (ParamType[Index] <> ptInteger)
776   then begin
777     FHasError := True;
778     AddError('Wrong amount of param');
779     exit(0);
780   end;
781   Result := FParams[Index].Num;
782 end;
783 
GetParamAsStringnull784 function TIdeMacroEventReader.GetParamAsString(Index: Integer): String;
785 begin
786   if (Index < 0) or (Index >= Length(FParams)) or (ParamType[Index] <> ptString)
787   then begin
788     FHasError := True;
789     AddError('Wrong amount of param');
790     exit('');
791   end;
792   Result := FParams[Index].Text;
793 end;
794 
TIdeMacroEventReader.GetParamTypenull795 function TIdeMacroEventReader.GetParamType(Index: Integer): TSynEventParamType;
796 begin
797   if (Index < 0) or (Index >= Length(FParams)) then begin
798     FHasError := True;
799     AddError('Wrong amount of param');
800     exit(ptString); // What to return here?
801   end;
802   Result := FParams[Index].ParamType;
803 end;
804 
PosToXYnull805 function TIdeMacroEventReader.PosToXY: TPoint;
806 var
807   f: TStringList;
808 begin
809   f := TStringList.Create;
810   f.Text := copy(FOrigText,1 ,FPos);
811   if f.Count = 0 then begin
812     Result.y := 1;
813     Result.x := 1;
814   end
815   else begin
816     Result.y := f.Count;
817     Result.x := length(f[f.Count-1])+1;
818   end;
819   f.Free;
820 end;
821 
AddErrornull822 function TIdeMacroEventReader.AddError(AMsg: string): Boolean;
823 var
824   p: TPoint;
825 begin
826   p := PosToXY;
827   FErrorText := FErrorText + Format('Error: %s at Line %d, Column %d', [AMsg, p.y, p.x]);
828   Result := False;
829 end;
830 
831 constructor TIdeMacroEventReader.Create(const Atext: String);
832 var
833   i: Integer;
834   s: String;
835 begin
836   FText := Atext;
837   FOrigText := Atext;
838   FHasError := False;
839   FErrorText := '';
840   FText := TrimRight(FText);
841   FPosCompensate := Length(FOrigText) - Length(FText);
842   FText := TrimLeft(FText);
843   i := length(FText);
844   if (i > 11) and
845      LazStartsText('begin', FText) and
846      (FText[6] in [#9,#10,#13,' ']) and
847      LazEndsText('end.', FText) and
848      (FText[i-4] in [#9,#10,#13,' '])
849   then begin
850     FText := copy(FText, 7, i-11);
851     FPosCompensate := FPosCompensate + 4;
852     s := TrimRight(FText);
853     FPosCompensate := FPosCompensate + Length(FText) - Length(s);
854     FText := Trim(FText);
855   end;
856 end;
857 
EventCommandnull858 function TIdeMacroEventReader.EventCommand: TSynEditorCommand;
859 begin
860   Result := FEventCommand;
861 end;
862 
ParamCountnull863 function TIdeMacroEventReader.ParamCount: Integer;
864 begin
865   Result := Length(FParams);
866 end;
867 
TIdeMacroEventReader.ParseNextEventnull868 function TIdeMacroEventReader.ParseNextEvent: Boolean;
869   procedure SkipNum(var i: integer);
870   begin
871     while (i <= Length(FText)) and (FText[i] in ['0'..'9']) do
872       inc(i);
873   end;
874   procedure SkipSpace(var i: integer);
875   begin
876     while (i <= Length(FText)) and (FText[i] in [' ', #9, #13, #10]) do
877       inc(i);
878   end;
879 var
880   c,i,j,k: Integer;
881   s: String;
882 begin
883   FEventName := '';
884   FText := TrimLeft(FText);
885 
886   Result := (FText <> '') and (not FHasError);
887   if not Result then exit;
888   Result := False;
889   FPos := Length(FOrigText) - Length(FText) - FPosCompensate;
890 
891   FHasError := True; // Assume the worst
892 
893   i := 1;
894   while (i <= Length(FText)) and (FText[i] in ['a'..'z','A'..'Z','0'..'9','_']) do
895     inc(i);
896   if i = 1 then exit(AddError('Expected Command, but found "'+UTF8Copy(FText,1,1)+'"'));
897 
898   s := Copy(FText, 1, i-1);
899   j:=0;
900   if not IdentToEditorCommand(s, j) then exit(AddError('Unknown Command "'+s+'"'));
901   FEventCommand := j;
902   FEventName := s;
903 
904   FPos := Length(FOrigText) - Length(FText) - FPosCompensate;
905   while (i <= Length(FText)) and (FText[i] in [' ', #9]) do
906     inc(i);
907   if (i > Length(FText)) then exit(AddError('Expected "(" or ";" bot got end of file'));
908 
909   SetLength(FParams, 0);
910   c := 0;
911 
912   if (FText[i] = '(') then begin
913     inc(i);
914     repeat
915       SkipSpace(i);
916       if (i > Length(FText)) then exit(AddError('Unexpected end of file in params'));
917 
918       if FText[i] in ['0'..'9'] then begin
919         // Parse number
920         j := i;
921         SkipNum(i);
922         SetLength(FParams, c + 1);
923         FParams[c].ParamType := ptInteger;
924         FParams[c].Num := StrToInt(copy(FText, j, i-j));
925         inc(c);
926       end
927       else
928       if FText[i] in ['#', ''''] then begin
929         // Parse string
930         s := '';
931         repeat
932           case FText[i] of
933             ' ',#9,#10,#13: inc(i);
934             '+': begin
935                 inc(i);
936                 if not (FText[i] in ['''', '#']) then exit(AddError('Expected string or char after +'));
937               end;
938             '#': begin
939                 inc(i);
940                 j := i;
941                 SkipNum(i);
942                 if i = j then exit(AddError('Expected number in string after #'));
943                 k := StrToInt(copy(FText, j, i-j));
944                 if k > 255 then exit(AddError('Argument to long'));
945                 s := s + chr(k);
946               end;
947             '''':  begin
948                 inc(i);
949                 repeat
950                   case FText[i] of
951                     '''': begin
952                         if (i+1 <= Length(FText)) and (FText[i+1] = '''') then begin
953                           s := s + '''';
954                           inc(i,2);
955                         end
956                         else begin
957                           inc(i);
958                           break;
959                         end;
960                       end;
961                     else begin
962                         s := s + FText[i];
963                         inc(i);
964                       end;
965                   end;
966                 until i > Length(FText);
967               end;
968             else
969               break;
970           end;
971         until i > Length(FText);
972 
973         SetLength(FParams, c + 1);
974         FParams[c].ParamType := ptString;
975         FParams[c].Text := s;
976         inc(c);
977       end
978       else
979       if FText[i] <> ')' then
980         exit(AddError('Unknown Arguent'));
981 
982       SkipSpace(i);
983       if (i >= Length(FText)) then exit(AddError('Missing ")"'));
984       if FText[i] = ')' then break;
985       if not(FText[i] = ',') then exit(AddError('Expected ","'));
986       inc(i);
987     until i > Length(FText);
988     inc(i);
989   end;
990 
991   if (i > Length(FText)) then exit(AddError('Missing ";"'));
992   if (FText[i] = ';') then begin
993     Delete(FText, 1, i);
994     Result := True;
995     FHasError := False;
996     exit;
997   end;
998   AddError('Unknown Error');
999 end;
1000 
1001 { TSynMacroEventWriter }
1002 
1003 constructor TIdeMacroEventWriter.Create;
1004 begin
1005   FUseLineFeed := False;
1006 end;
1007 
1008 procedure TIdeMacroEventWriter.BeginEvent;
1009 begin
1010   FCmdName := '';
1011   FParams := '';
1012 end;
1013 
1014 procedure TIdeMacroEventWriter.FinishEvent;
1015 begin
1016   FText := FText + FCmdName;
1017   if FParams <> '' then
1018     FText := FText + '(' + FParams + ')';
1019   FText := FText + ';';
1020   if FUseLineFeed then
1021     FText := FText + LineEnding;
1022 end;
1023 
1024 procedure TIdeMacroEventWriter.WriteEventCommand(const ACmd: TSynEditorCommand);
1025 begin
1026   EditorCommandToIdent(ACmd, FCmdName);
1027 end;
1028 
1029 procedure TIdeMacroEventWriter.WriteEventParam(const AParam: string);
1030 var
1031   s: String;
1032   i: Integer;
1033   InQuotes: Boolean;
1034 begin
1035   if FParams <> '' then
1036     FParams := FParams + ', ';
1037   s := '';
1038   InQuotes := False;
1039   for i := 1 to length(AParam) do
1040     case AParam[i] of
1041       #0..#31: begin
1042           if InQuotes then s := s + '''';
1043           InQuotes := False;
1044           s := s + '#' + IntToStr(ord(AParam[i]));
1045         end;
1046       '''': begin
1047           if not InQuotes then s := s + '''';
1048           InQuotes := True;
1049           s := s + '''''';
1050         end;
1051       else begin
1052           if not InQuotes then s := s + '''';
1053           InQuotes := True;
1054           s := s + AParam[i];
1055         end;
1056     end;
1057   if InQuotes then s := s + '''';
1058   FParams := FParams + s;
1059 end;
1060 
1061 procedure TIdeMacroEventWriter.WriteEventParam(const AParam: integer);
1062 begin
1063   if FParams <> '' then
1064     FParams := FParams + ', ';
1065   FParams := FParams + IntToStr(AParam);
1066 end;
1067 
1068 { TMacroListView }
1069 
1070 procedure TMacroListView.btnRenameClick(Sender: TObject);
1071 var
1072   s: String;
1073   M: TEditorMacro;
1074 begin
1075   if lbMacroView.ItemIndex < 0 then exit;
1076   M := CurrentEditorMacroList.Macros[lbMacroView.ItemIndex];
1077   s := M.MacroName;
1078   if InputQuery(lisNewMacroname2, Format(lisEnterNewNameForMacroS, [m.MacroName]), s)
1079   then begin
1080     while (s <> '') and (CurrentEditorMacroList.IndexOfName(s) >= 0) do begin
1081       case IDEMessageDialog(lisDuplicateName, lisAMacroWithThisNameAlreadyExists, mtWarning,
1082         mbOKCancel) of
1083         mrOK:
1084           if not InputQuery(lisNewMacroname2, Format(lisEnterNewNameForMacroS, [m.MacroName]), s)
1085           then s := '';
1086         else
1087           s := '';
1088       end;
1089     end;
1090 
1091     if s <> '' then
1092       M.MacroName := s;
1093     UpdateDisplay;
1094   end;
1095 end;
1096 
1097 procedure TMacroListView.btnPlayClick(Sender: TObject);
1098 var
1099   i: Integer;
1100   M: TEditorMacro;
1101   se: TSourceEditorInterface;
1102 begin
1103   if ActiveEditorMacro <> nil then exit;
1104   if lbMacroView.ItemIndex < 0 then exit;
1105   se := SourceEditorManagerIntf.ActiveEditor;
1106   if se = nil then Exit;
1107 
1108   i := 1;
1109   if chkRepeat.Checked then i := edRepeat.Value;
1110   FIsPlaying := True;
1111   UpdateButtons;
1112 
1113   M := CurrentEditorMacroList.Macros[lbMacroView.ItemIndex];
1114   try
1115     while i > 0 do begin
1116       M.PlaybackMacro(TCustomSynEdit(se.EditorControl));
1117       Application.ProcessMessages;
1118       dec(i);
1119       if not FIsPlaying then break;
1120     end;
1121   finally
1122     FIsPlaying := False;
1123     UpdateButtons;
1124   end;
1125 
1126 end;
1127 
1128 procedure TMacroListView.btnDeleteClick(Sender: TObject);
1129 var
1130   m: TEditorMacro;
1131 begin
1132   if lbMacroView.ItemIndex < 0 then exit;
1133   if IDEMessageDialog(lisReallyDelete, lisDeleteSelectedMacro, mtConfirmation, [
1134     mbYes, mbNo]) = mrYes
1135   then begin
1136     if SelectedEditorMacro = CurrentEditorMacroList.Macros[lbMacroView.ItemIndex] then begin
1137       SelectedEditorMacro := nil;
1138     end;
1139     m := CurrentEditorMacroList.Macros[lbMacroView.ItemIndex];
1140     CurrentEditorMacroList.Delete(lbMacroView.ItemIndex);
1141     m.Free;
1142     if CurrentEditorMacroList = EditorMacroListProj then Project1.Modified := True;
1143     if CurrentEditorMacroList = EditorMacroListGlob then MainIDEInterface.SaveEnvironment(False);
1144     UpdateDisplay;
1145   end;
1146 end;
1147 
1148 procedure TMacroListView.btnEditClick(Sender: TObject);
1149 var
1150   M: TEditorMacro;
1151 begin
1152   if lbMacroView.ItemIndex < 0 then exit;
1153   M := CurrentEditorMacroList.Macros[lbMacroView.ItemIndex];
1154   if M = nil then exit;
1155   LazarusIDE.DoOpenEditorFile(
1156     EditorMacroVirtualDrive+MacroListToName(CurrentEditorMacroList)+'|'+M.MacroName,
1157     -1, -1, [ofVirtualFile, ofInternalFile]);
1158 end;
1159 
1160 procedure TMacroListView.btnRecordClick(Sender: TObject);
1161 var
1162   se: TSourceEditorInterface;
1163 begin
1164   se := SourceEditorManagerIntf.ActiveEditor;
1165   if se = nil then Exit;
1166   if (ActiveEditorMacro = nil) and (EditorMacroForRecording.State = emStopped) then
1167     EditorMacroForRecording.RecordMacro(TCustomSynEdit(se.EditorControl))
1168   else
1169   if EditorMacroForRecording.State = emRecording then
1170     EditorMacroForRecording.Pause
1171   else
1172   if EditorMacroForRecording.State = emRecPaused then
1173     EditorMacroForRecording.Resume;
1174   se.EditorControl.SetFocus;
1175 end;
1176 
1177 procedure TMacroListView.btnRecordStopClick(Sender: TObject);
1178 begin
1179   FIsPlaying := False;
1180   EditorMacroForRecording.Stop;
1181 end;
1182 
1183 procedure TMacroListView.btnSelectClick(Sender: TObject);
1184 begin
1185   if ActiveEditorMacro <> nil then exit;
1186   if lbMacroView.ItemIndex >= 0 then
1187     SelectedEditorMacro := CurrentEditorMacroList.Macros[lbMacroView.ItemIndex]
1188   else
1189     SelectedEditorMacro:= nil;
1190   UpdateDisplay;
1191 end;
1192 
1193 procedure TMacroListView.btnSetKeysClick(Sender: TObject);
1194 var
1195   i: integer;
1196   M: TEditorMacro;
1197 begin
1198   if lbMacroView.ItemIndex < 0 then exit;
1199   M := CurrentEditorMacroList.Macros[lbMacroView.ItemIndex];
1200 
1201   if (M.KeyBinding = nil) or (M.KeyBinding.IdeCmd = nil) or
1202      not(M.KeyBinding.IdeCmd is TKeyCommandRelation)
1203   then // only for error macros
1204     exit;
1205 
1206   i := (IDECommandList as TKeyCommandRelationList).IndexOf(M.KeyBinding.IdeCmd as TKeyCommandRelation);
1207   if (i >= 0) then
1208     if ShowKeyMappingEditForm(i, (IDECommandList as TKeyCommandRelationList)) = mrOK then begin
1209       if CurrentEditorMacroList = EditorMacroListProj then Project1.Modified := True;
1210       if CurrentEditorMacroList = EditorMacroListGlob then MainIDEInterface.SaveEnvironment(False);
1211     end;
1212   UpdateDisplay;
1213   if OnKeyMapReloaded <> nil then OnKeyMapReloaded();
1214 end;
1215 
1216 procedure TMacroListView.BtnWarnCloseClick(Sender: TObject);
1217 begin
1218   PanelWarnings.Visible := False;
1219 end;
1220 
1221 procedure TMacroListView.DoMacroStateChanged(Sender: TObject);
1222 begin
1223   if OnEditorMacroStateChange <> nil then
1224     OnEditorMacroStateChange(Sender);
1225 end;
1226 
1227 procedure TMacroListView.FormActivate(Sender: TObject);
1228 begin
1229   lbMacroView.HideSelection := Active;
1230 end;
1231 
1232 procedure TMacroListView.HelpButtonClick(Sender: TObject);
1233 begin
1234   LazarusHelp.ShowHelpForIDEControl(Self);
1235 end;
1236 
1237 procedure TMacroListView.lbMacroViewSelectItem(Sender: TObject; Item: TListItem;
1238   Selected: Boolean);
1239 begin
1240   UpdateButtons;
1241 end;
1242 
1243 procedure TMacroListView.mnExportClick(Sender: TObject);
1244 var
1245   Conf: TXMLConfig;
1246 begin
1247   if lbMacroView.ItemIndex < 0 then exit;
1248 
1249   if SaveDialog1.Execute then begin
1250     Conf := TXMLConfig.Create(SaveDialog1.FileName);
1251     try
1252       Conf.Clear;
1253       Conf.SetValue('EditorMacros/Count', 1);
1254       CurrentEditorMacroList.Macros[lbMacroView.ItemIndex].WriteToXmlConf(Conf, 'EditorMacros/Macro1/');
1255     finally
1256       Conf.Free;
1257     end;
1258   end;
1259 end;
1260 
1261 procedure TMacroListView.mnImportClick(Sender: TObject);
1262 var
1263   Conf: TXMLConfig;
1264   NewMacro: TEditorMacro;
1265 begin
1266   if OpenDialog1.Execute then begin
1267     Conf := TXMLConfig.Create(OpenDialog1.FileName);
1268     try
1269       NewMacro := EditorMacroPlayerClass.Create(nil);
1270       NewMacro.OnStateChange := @DoMacroStateChanged;
1271       NewMacro.OnChange := @DoMacroContentChanged;
1272       NewMacro.ReadFromXmlConf(Conf, 'EditorMacros/Macro1/');
1273       if not NewMacro.IsEmpty then begin
1274         CurrentEditorMacroList.Add(NewMacro);
1275         if CurrentEditorMacroList = EditorMacroListProj then Project1.Modified := True;
1276         if CurrentEditorMacroList = EditorMacroListGlob then MainIDEInterface.SaveEnvironment(False);
1277       end
1278       else
1279         NewMacro.Free;
1280     finally
1281       Conf.Free;
1282     end;
1283   end;
1284 end;
1285 
1286 procedure TMacroListView.tbIDEClick(Sender: TObject);
1287 begin
1288   CurrentEditorMacroList := EditorMacroListGlob;
1289   UpdateDisplay;
1290 end;
1291 
1292 procedure TMacroListView.tbMoveIDEClick(Sender: TObject);
1293 var
1294   i: Integer;
1295 begin
1296   if (lbMacroView.ItemIndex < 0) or (CurrentEditorMacroList = EditorMacroListGlob) then exit;
1297   i := lbMacroView.ItemIndex;
1298   EditorMacroListGlob.Add(CurrentEditorMacroList.Macros[i]);
1299   CurrentEditorMacroList.Delete(i);
1300   if CurrentEditorMacroList = EditorMacroListProj then Project1.Modified := True;
1301   MainIDEInterface.SaveEnvironment(False);
1302   UpdateDisplay;
1303 end;
1304 
1305 procedure TMacroListView.tbMoveProjectClick(Sender: TObject);
1306 var
1307   i: Integer;
1308 begin
1309   if (lbMacroView.ItemIndex < 0) or (CurrentEditorMacroList = EditorMacroListProj) then exit;
1310   i := lbMacroView.ItemIndex;
1311   EditorMacroListProj.Add(CurrentEditorMacroList.Macros[i]);
1312   CurrentEditorMacroList.Delete(i);
1313   Project1.Modified := True;
1314   if CurrentEditorMacroList = EditorMacroListGlob then MainIDEInterface.SaveEnvironment(False);
1315   UpdateDisplay;
1316 end;
1317 
1318 procedure TMacroListView.tbProjectClick(Sender: TObject);
1319 begin
1320   CurrentEditorMacroList := EditorMacroListProj;
1321   UpdateDisplay;
1322 end;
1323 
1324 procedure TMacroListView.tbRecordedClick(Sender: TObject);
1325 begin
1326   CurrentEditorMacroList := EditorMacroListRec;
1327   UpdateDisplay;
1328 end;
1329 
1330 procedure TMacroListView.DoOnMacroListChange(Sender: TObject);
1331 begin
1332   UpdateDisplay;
1333 
1334   if Sender = EditorMacroListProj then
1335     Project1.SessionModified := True;
1336 end;
1337 
1338 procedure TMacroListView.DoMacroContentChanged(Sender: TObject);
1339 begin
1340   if FIgnoreMacroChanges then exit;
1341 
1342   if EditorMacroListProj.IndexOf(Sender as TEditorMacro) >= 0 then
1343     Project1.Modified := True;
1344   if EditorMacroListGlob.IndexOf(Sender as TEditorMacro) >= 0 then
1345     MainIDEInterface.SaveEnvironment(False);
1346 end;
1347 
1348 procedure TMacroListView.UpdateDisplay;
1349 var
1350   NewItem: TListItem;
1351   i, idx: Integer;
1352   M: TEditorMacro;
1353 begin
1354   idx := lbMacroView.ItemIndex;
1355   lbMacroView.Items.Clear;
1356 
1357   for i := 0 to CurrentEditorMacroList.Count - 1 do begin
1358     M := CurrentEditorMacroList.Macros[i];
1359     NewItem := lbMacroView.Items.Add;
1360 
1361     if m.KeyBinding <> nil then
1362       NewItem.Caption := M.MacroName + M.KeyBinding.ShortCutAsText
1363     else
1364       NewItem.Caption := M.MacroName;
1365 
1366     // Image
1367     if M.IsInvalid then
1368       NewItem.ImageIndex := FImageErr
1369     else
1370     if (m = CurrentRecordingMacro) then
1371       NewItem.ImageIndex := FImageRec
1372     else
1373     if (CurrentRecordingMacro = nil) then begin // If recording, then recorder is selected
1374       If (M = ActiveEditorMacro) and (M.State = emPlaying) then
1375         NewItem.ImageIndex := FImagePlay
1376       else
1377       if (M = SelectedEditorMacro) then
1378         NewItem.ImageIndex := FImageSel;
1379     end;
1380   end;
1381   if idx < lbMacroView.Items.Count then
1382     lbMacroView.ItemIndex := idx
1383   else
1384     lbMacroView.ItemIndex := -1;
1385 
1386   lbMacroViewSelectItem(nil, nil, False);
1387   UpdateButtons;
1388 end;
1389 
1390 procedure TMacroListView.UpdateButtons;
1391 var
1392   IsSel, IsErr, IsStopped: Boolean;
1393   M: TEditorMacro;
1394   RecState: TEditorMacroState;
1395 begin
1396   IsSel := (lbMacroView.ItemIndex >= 0);
1397   IsStopped := (ActiveEditorMacro = nil);
1398   if IsSel then
1399     M := CurrentEditorMacroList.Macros[lbMacroView.ItemIndex];
1400   IsErr := IsSel and M.IsInvalid;
1401   RecState := emStopped;
1402   if EditorMacroForRecording <> nil then
1403     RecState := EditorMacroForRecording.State;
1404 
1405   btnSelect.Enabled  := IsStopped and IsSel and (not FIsPlaying) and (not IsErr);
1406   btnRename.Enabled  := IsStopped and IsSel and (not FIsPlaying) and (not IsErr);
1407   btnEdit.Enabled    := IsStopped and IsSel and (not FIsPlaying);
1408   btnSetKeys.Enabled := IsStopped and IsSel and (not FIsPlaying) and (not IsErr);
1409   btnDelete.Enabled  := IsStopped and IsSel and (not FIsPlaying);
1410 
1411   btnPlay.Enabled    := IsStopped and IsSel and (not FIsPlaying) and (not IsErr);
1412   chkRepeat.Enabled  := IsStopped and (not FIsPlaying);
1413   edRepeat.Enabled   := IsStopped and (not FIsPlaying);
1414 
1415   btnRecord.Enabled := Assigned(SourceEditorManagerIntf.ActiveEditor)
1416                   and (RecState in [emStopped, emRecPaused, emRecording])
1417                   and (not FIsPlaying);
1418   btnRecordStop.Enabled := (not IsStopped) or FIsPlaying;
1419 
1420   if (RecState = emRecording) then
1421     btnRecord.Caption := lisPause
1422   else if (RecState = emRecPaused) then
1423     btnRecord.Caption := lisContinue
1424   else
1425     btnRecord.Caption := lisRecord;
1426 
1427   mnImport.Enabled := IsStopped and (not FIsPlaying);
1428   mnExport.Enabled := IsStopped and IsSel and (not FIsPlaying);
1429 
1430   tbMoveProject.Visible := CurrentEditorMacroList <> EditorMacroListProj;
1431   tbMoveProject.Enabled := IsStopped and IsSel and (not FIsPlaying);
1432   tbMoveIDE.Visible := CurrentEditorMacroList <> EditorMacroListGlob;
1433   tbMoveIDE.Enabled := IsStopped and IsSel and (not FIsPlaying);
1434 
1435   Update;
1436 end;
1437 
MacroByFullNamenull1438 function TMacroListView.MacroByFullName(AName: String): TEditorMacro;
1439 const
1440   FolderStart = length(EditorMacroVirtualDrive)+1;
1441   NameStart = FolderStart+length('PRJ|');
1442 var
1443   Alist: TEditorMacroList;
1444   i: Integer;
1445 begin
1446   Result := nil;
1447   If not LazStartsStr(EditorMacroVirtualDrive, AName) or
1448      (copy(AName, NameStart-1, 1) <> '|')
1449   then exit;
1450   Alist := NameToMacroList(copy(AName, FolderStart, 3));
1451   if (Alist = nil) then exit;
1452   i := Alist.IndexOfName(copy(AName, NameStart, length(AName)));
1453   if i < 0 then exit;
1454   Result := Alist.Macros[i];
1455 end;
1456 
1457 procedure TMacroListView.DoEditorMacroStateChanged;
1458 begin
1459   UpdateDisplay;
1460 end;
1461 
1462 constructor TMacroListView.Create(TheOwner: TComponent);
1463 begin
1464   inherited Create(TheOwner);
1465   FIgnoreMacroChanges := False;
1466 
1467   Caption := lisEditorMacros;
1468   EditorMacroListRec.OnChange := @DoOnMacroListChange;
1469   EditorMacroListProj.OnChange := @DoOnMacroListChange;
1470   EditorMacroListGlob.OnChange := @DoOnMacroListChange;
1471 
1472   tbRecorded.Caption := lisRecordedMacros;
1473   tbProject.Caption := lisProjectMacro;
1474   tbIDE.Caption := lisIDE;
1475   tbRecorded.Hint := lisNewRecordedMacrosNotToBeSaved;
1476   tbProject.Hint := lisSavedWithProjectSession;
1477   tbIDE.Hint := lisSavedWithIDESettings;
1478   tbMoveProject.Caption := lisProjectMacro;
1479   tbMoveIDE.Caption := lisIDE;
1480   lbMoveTo.Caption := lisMoveTo + '  '; // Anchors do not work here. Use spaces.
1481 
1482   btnSelect.Caption := lisMakeCurrent;
1483   btnRename.Caption := lisRename2;
1484   btnSetKeys.Caption := lisEditKey;
1485   btnEdit.Caption   := lisEdit;
1486   btnDelete.Caption := lisDelete;
1487   btnPlay.Caption := lisPlay;
1488   chkRepeat.Caption := lisRepeat;
1489   btnRecord.Caption := lisRecord;
1490   btnRecordStop.Caption := lisStop;
1491 
1492   SaveDialog1.Title := lisSaveMacroAs;
1493   OpenDialog1.Title := lisLoadMacroFrom;
1494   mnImport.Caption := lisDlgImport;
1495   mnExport.Caption := lisDlgExport;
1496 
1497   lbMacroView.SmallImages := IDEImages.Images_16;
1498   FImageRec := IDEImages.LoadImage('Record');  // red dot
1499   FImagePlay := IDEImages.LoadImage('menu_run');  // green triangle
1500   FImageSel := IDEImages.LoadImage('arrow_right');
1501   FImageErr := IDEImages.LoadImage('state_error');
1502   FIsPlaying := False;
1503 
1504   BtnWarnClose.Images := IDEImages.Images_16;
1505   BtnWarnClose.ImageIndex := IDEImages.LoadImage('menu_close');
1506 
1507   UpdateDisplay;
1508 end;
1509 
1510 destructor TMacroListView.Destroy;
1511 begin
1512   inherited Destroy;
1513 end;
1514 
1515 { TEditorMacroList }
1516 
TEditorMacroList.GetMacrosnull1517 function TEditorMacroList.GetMacros(Index: Integer): TEditorMacro;
1518 begin
1519   Result := TEditorMacro(FList[Index]);
1520 end;
1521 
1522 procedure TEditorMacroList.DoChanged;
1523 begin
1524   if Assigned(FOnChange) then
1525     FOnChange(Self);
1526 end;
1527 
1528 procedure TEditorMacroList.DoAdded(AMacro: TEditorMacro);
1529 begin
1530   if Assigned(FOnAdded) then
1531     FOnAdded(Self, AMacro);
1532 end;
1533 
1534 procedure TEditorMacroList.DoRemove(AMacro: TEditorMacro);
1535 begin
1536   if Assigned(FOnRemove) then
1537     FOnRemove(Self, AMacro);
1538 end;
1539 
1540 constructor TEditorMacroList.Create;
1541 begin
1542   FList := TList.Create;
1543 end;
1544 
1545 destructor TEditorMacroList.Destroy;
1546 begin
1547   FOnChange := nil;
1548   ClearAndFreeMacros;
1549   FreeAndNil(FList);
1550   inherited Destroy;
1551 end;
1552 
1553 procedure TEditorMacroList.WriteToXmlConf(AConf: TXMLConfig; const APath: String);
1554 var
1555   i: Integer;
1556 begin
1557   AConf.SetDeleteValue(APath + 'EditorMacros/Count', Count, 0);
1558   for i := 0 to Count - 1 do
1559     Macros[i].WriteToXmlConf(AConf, 'EditorMacros/Macro'+IntToStr(i+1)+'/');
1560 end;
1561 
1562 procedure TEditorMacroList.ReadFromXmlConf(AConf: TXMLConfig; const APath: String);
1563 var
1564   c, i: Integer;
1565   NewMacro: TEditorMacro;
1566 begin
1567   ClearAndFreeMacros;
1568   c := AConf.GetValue(APath + 'EditorMacros/Count', 0);
1569   for i := 0 to c -1 do begin
1570     NewMacro := EditorMacroPlayerClass.Create(nil);
1571     NewMacro.OnStateChange := @MacroListViewer.DoMacroStateChanged;
1572     NewMacro.OnChange := @MacroListViewer.DoMacroContentChanged;
1573     try
1574       NewMacro.ReadFromXmlConf(AConf, 'EditorMacros/Macro'+IntToStr(i+1)+'/');
1575     finally
1576       Add(NewMacro)
1577     end;
1578   end;
1579 end;
1580 
1581 procedure TEditorMacroList.ClearAndFreeMacros;
1582 var
1583   i: Integer;
1584 begin
1585   for i := 0 to Count - 1 do
1586     Macros[i].Free;
1587   FList.Clear;
1588 end;
1589 
Countnull1590 function TEditorMacroList.Count: Integer;
1591 begin
1592   Result := FList.Count;
1593 end;
1594 
IndexOfnull1595 function TEditorMacroList.IndexOf(AMacro: TEditorMacro): Integer;
1596 begin
1597   Result := FList.IndexOf(AMacro);
1598 end;
1599 
IndexOfNamenull1600 function TEditorMacroList.IndexOfName(AName: String): Integer;
1601 begin
1602   Result := Count - 1;
1603   while Result >= 0 do
1604     if Macros[Result].MacroName = AName
1605     then break
1606     else dec(Result);
1607 end;
1608 
UniqNamenull1609 function TEditorMacroList.UniqName(AName: String): String;
1610 var
1611   i: Integer;
1612 begin
1613   Result := AName;
1614   if IndexOfName(AName) < 0 then exit;
1615   i := 1;
1616   while IndexOfName(AName+'_'+IntToStr(i)) >= 0 do
1617     inc(i);
1618   Result := AName+'_'+IntToStr(i);
1619 end;
1620 
Addnull1621 function TEditorMacroList.Add(AMacro: TEditorMacro): Integer;
1622 begin
1623   AMacro.MacroName := UniqName(AMacro.MacroName);
1624   Result := FList.Add(AMacro);
1625   DoAdded(AMacro);
1626   DoChanged;
1627 end;
1628 
1629 procedure TEditorMacroList.Delete(AnIndex: Integer);
1630 begin
1631   DoRemove(Macros[AnIndex]);
1632   FList.Delete(AnIndex);
1633   DoChanged;
1634 end;
1635 
1636 procedure TEditorMacroList.Remove(AMacro: TEditorMacro);
1637 begin
1638   DoRemove(AMacro);
1639   FList.Remove(AMacro);
1640   DoChanged;
1641 end;
1642 
1643 // itmMacroListView.enabled
1644 
1645 {$R *.lfm}
1646 
1647 initialization
1648   if EditorMacroPlayerClass = nil then
1649     EditorMacroPlayerClass := TIdeEditorMacro;
1650   if DefaultBindingClass = nil then
1651   DefaultBindingClass := TIdeEditorMacroKeyBinding;
1652   EditorMacroListRec := TEditorMacroList.Create;
1653   EditorMacroListProj := TEditorMacroList.Create;
1654   EditorMacroListGlob := TEditorMacroList.Create;
1655   CurrentEditorMacroList := EditorMacroListRec;
1656 
1657   MacroListViewerWarningChanged := @DoMacroListViewerWarningChanged;
1658 
1659 finalization
1660   CurrentEditorMacroList := nil;
1661   FreeAndNil(EditorMacroListRec);
1662   FreeAndNil(EditorMacroListProj);
1663   FreeAndNil(EditorMacroListGlob);
1664 
1665 end.
1666 
1667