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