Open Tools API その2:メニューの差し替えとノーティファイアインターフェース

IDEのメニューを差し替えるには、IDEそのもののサービスであるINTAServicesのMainMenuプロパティを取得する。
これはTMainMenu型なので、通常のメニューにアクセスのと全く変わりなし。

procedure TFilterWizard.DataModuleCreate(Sender: TObject);
var
  i: Integer;
  ItemIndex: Integer;
  InsertPosition: Integer;
  IDEMainMenu: TMainMenu;
  ToolsMenu: TMenuItem;
begin

  // メインメニューの[ツール|オプション]を探す
  IDEMainMenu := (BorlandIDEServices as INTAServices).MainMenu;
  ToolsMenu := nil;
  with IDEMainMenu do begin
    for I := 0 to Items.Count - 1 do begin
      if AnsiSameText(Items[I].Name, 'ToolsMenu') then
        ToolsMenu := Items[I];
    end;
  end;

  InsertPosition := ToolsMenu.Count;
  for I := 0 to ToolsMenu.Count-1 do begin
    if AnsiSameText(ToolsMenu.Items[I].Name, 'ToolsOptionsItem') then
    begin
      InsertPosition := I;
      Break;
    end;
  end;

  // PopUpMenuからIDEの[ツール|オプション]へMenuItemを移動
  ItemIndex := PopupMenu1.Items.IndexOf(miOption);
  PopupMenu1.Items.Delete(ItemIndex);
  ToolsMenu.Insert(InsertPosition, miOption);
end;

ソースエディタのメニューを差し替えるには、ソースエディタがアクティブになった段階で行うのだけど、これを検知するノーティファイアの登録は二段階で行わないと行けない。

  1. IDEに(何らかの)ファイルがロードされたことを通知するノーティファイア
  2. ソースエディタがアクティブになったことを通知するノーティファイア

まず、IDEのノーティファイアとソースエディタのノーティファイアの定義。

  // IDEのノーティファイア
  TIDENotifier = class(TNotifierObject, IOTANotifier, IOTAIDENotifier)
  private
    procedure FileNotification(NotifyCode: TOTAFileNotification; const FileName: string; var Cancel: Boolean);
    procedure BeforeCompile(const Project: IOTAProject; var Cancel: Boolean); overload;
    procedure AfterCompile(Succeeded: Boolean); overload;
  end;

  // ソースエディタのノーティファイア
  TSourceEditorNotifier = class(TNotifierObject, IOTANotifier, IOTAEditorNotifier)
  private
    FEditor: IOTASourceEditor;
    FIndex: Integer;

    procedure Destroyed;
    procedure ViewActivated(const View: IOTAEditView);
    procedure ViewNotification(const View: IOTAEditView; Operation: TOperation);
  public
    constructor Create(AEditor: IOTASourceEditor);
    destructor Destroy; override;
end;

Registerでまず、IDEにノーティファイアを登録する。

procedure Register;
var
  Services: IOTAServices;
begin
  Services := BorlandIDEServices as IOTAServices;
  Assert(Assigned(Services), 'IOTAServices not available');
  IDENotifierIndex := Services.AddNotifier(TIdeNotifier.Create());
end;

IDEでファイルに何らかの操作が発生するとIDEのノーティファイアのFileNotificationが呼ばれる。ファイルがオープンされるとイベントの種別としてofnFileOpenedが渡されるので、ここでソースエディタにノーティファイアを登録する。

procedure TIdeNotifier.FileNotification(NotifyCode: TOTAFileNotification; const FileName: string; var Cancel: Boolean);
var
  ModuleServices: IOTAModuleServices;
  Module: IOTAModule;
begin
  if NotifyCode =  ofnFileOpened then begin
    ModuleServices := BorlandIDEServices as IOTAModuleServices;
    Module := ModuleServices.FindModule(FileName);
    if Assigned(Module) then begin
      InstallSourceEditorNotifiers(Module);
    end;
  end;
end;

// ソースエディタにノーティファイアを登録
procedure InstallSourceEditorNotifiers(Module: IOTAModule);
var
  I: Integer;
  SourceEditor: IOTASourceEditor;
begin
  for I := 0 to Module.ModuleFileCount - 1 do
    if Supports(Module.ModuleFileEditors[I], IOTASourceEditor, SourceEditor) then
    begin
      SourceEditorNotifiers.Add(TSourceEditorNotifier.Create(SourceEditor));
      SourceEditor := nil;
    end;
end;

エディタがアクティブになるとノーティファイアのViewActivatedが呼ばれる。ここでソースエディタのメニューを取得しメニューアイテムを登録。注意しなければならないのはメニューアイテムの「親」は一つだけなので、アクティブで無くなったソースエディタからメニューアイテムを除去し、アクティブになったソースエディタのメニューにメニューアイテムを登録し直す。

procedure TSourceEditorNotifier.ViewActivated(const View: IOTAEditView);
var
  EditWindow: INTAEditWindow;
  EditWindowForm: TCustomForm;
  EditorLocalMenu: TComponent;
  ParentMenu: TMenu;
begin
  EditWindow := View.GetEditWindow;
  if not Assigned(EditWindow) then
    Exit;

  EditWindowForm := EditWindow.Form;
  if not Assigned(EditWindowForm) then
    Exit;

  // エディタのメニューを取得
  EditorLocalMenu := EditWindowForm.FindComponent('EditorLocalMenu');
  if not Assigned(EditorLocalMenu) then
    Exit;

  try
    if (EditorLocalMenu is TMenu) then
    begin
      with FilterWizard do begin

        // アクティブで無くなったコードエディタからメニューアイテムを削除
        ParentMenu := miExecFormatter.GetParentMenu;
        if Assigned(ParentMenu) then
          ParentMenu.Items.Remove(miExecFormatter);

        // アクティブになったコードエディタのメニューにメニューアイテムを登録
        TMenu(EditorLocalMenu).Items.Add(miExecFormatter);
      end;
    end;
  except
    raise;
  end;
end;

ノーティファイアを登録したら、最後に後始末をしなければならないので、finalizationで後始末。

// IDEに登録してあるノーティファイアを削除する
procedure RemoveNotifier;
var
  Services: IOTAServices;
begin
  if IDENotifierIndex <> -1 then
  begin
    Services := BorlandIDEServices as IOTAServices;
    Assert(Assigned(Services), 'IOTAServices not available');
    Services.RemoveNotifier(IDENotifierIndex);
  end;
end;

// ソースエディタに登録してあるノーティファイアを削除
procedure ClearSourceEditorNotifiers;
var
  I: Integer;
begin
  if Assigned(SourceEditorNotifiers) then
    for I := SourceEditorNotifiers.Count - 1 downto 0 do
      TSourceEditorNotifier(SourceEditorNotifiers[I]).Destroyed;
end;

finalization
  RemoveNotifier;
  ClearSourceEditorNotifiers;
  FreeAndNil(SourceEditorNotifiers);