第5回 Delphi talksに参加してきた

この手のイベントに参加するのは本当に久しぶり。楽しい時間を過ごせました。
LTのネタは諸事情により2年3ヶ月ほど塩漬け状態な、コードフォーマッタープラグインネタ。

あまり役に立たない、昨日使用したプレゼンを公開します。
ちなみに、プラグインそのもの(RAD Studio XE用)はhttp://a7m.sakura.ne.jp/APPS/UncrustifyRS_1.00.7zにて公開中。ソースはMPLで配布しているので、ご自由にお使いください。ビルドし直せばXE4とかでも動くはず。(多分)
デブキャンでネタにしたOpen Tools APIについてはここで公開しています。

RAD Studio XEとF-Secure Internet Security 2011の相性問題

メインマシンのセキュリティソフトをウイルスバスターからエフセキュア インターネット セキュリティ 2011に乗り換えたのだけど、RAD Studio XEを起動すると「使用許諾コードが不正」と判断されてRAD Studioが起動しなかった
これは、エフセキュア インターネット セキュリティのディープガードに由来するもので、以下の手順でBDS.EXEをディープガードの検索対象から外せばOK。

  1. F-Secure Internet Security 2011を開く
  2. [コンピュータ|ウイルスとスパイウェア スキャン]を選択
  3. [除外したオブジェクトを表示]をクリック
  4. [オブジェクト]タブを選択
  5. [追加]をクリックして、"C:\Program Files (x86)\Embarcadero\RAD Studio\8.0\bin\bds.exe"を選択
  6. [OK]をクリック
  7. [OK]をクリック
  8. [閉じる]をクリック

以上でRAD Studioが検索対象から外れるので、RAD Studioが問題なく実行される。

Universal Character Set Detector C LibraryをDelphi XEから使用する

C++Builderで試したMozillaエンコーディング自動判別ライブラリである「universalchardet」をDLL化したのをDelphiで動作させてみました。バイナリはhttp://a7m.sakura.ne.jp/SOURCE/universalchardet-CB.7zに用意したもので、C++Builder XE(BCC32.EXE 6.31)でコンパイル

Delphiでuniversalchardet.dllを使うためには、DLLの宣言を記述したユニットを作成しなければならない。

unit universalchardet;

interface

type
  chardet_t = Pointer;

const
  CHARDET_RESULT_OK = 0;
  CHARDET_RESULT_NOMEMORY = -1;
  CHARDET_RESULT_INVALID_DETECTOR = -2;
  CHARDET_MAX_ENCODING_NAME = 64;

  CHARDET_ENCODING_ISO_2022_JP = 'ISO-2022-JP';
  CHARDET_ENCODING_ISO_2022_CN = 'ISO-2022-CN';
  CHARDET_ENCODING_ISO_2022_KR = 'ISO-2022-KR';
  CHARDET_ENCODING_ISO_8859_5 = 'ISO-8859-5';
  CHARDET_ENCODING_ISO_8859_7 = 'ISO-8859-7';
  CHARDET_ENCODING_ISO_8859_8 = 'ISO-8859-8';
  CHARDET_ENCODING_BIG5 = 'BIG5';
  CHARDET_ENCODING_GB18030 = 'GB18030';
  CHARDET_ENCODING_EUC_JP = 'EUC-JP';
  CHARDET_ENCODING_EUC_KR = 'EUC-KR';
  CHARDET_ENCODING_EUC_TW = 'EUC-TW';
  CHARDET_ENCODING_SHIFT_JIS = 'SHIFT_JIS';
  CHARDET_ENCODING_IBM855 = 'IBM855';
  CHARDET_ENCODING_IBM866 = 'IBM866';
  CHARDET_ENCODING_KOI8_R = 'KOI8-R';
  CHARDET_ENCODING_MACCYRILLIC = 'MACCYRILLIC';
  CHARDET_ENCODING_WINDOWS_1251 = 'WINDOWS-1251';
  CHARDET_ENCODING_WINDOWS_1252 = 'WINDOWS-1252';
  CHARDET_ENCODING_WINDOWS_1253 = 'WINDOWS-1253';
  CHARDET_ENCODING_WINDOWS_1255 = 'WINDOWS-1255';
  CHARDET_ENCODING_UTF_8 = 'UTF-8';
  CHARDET_ENCODING_UTF_16BE = 'UTF-16BE';
  CHARDET_ENCODING_UTF_16LE = 'UTF-16LE';
  CHARDET_ENCODING_UTF_32BE = 'UTF-32BE';
  CHARDET_ENCODING_UTF_32LE = 'UTF-32LE';
  CHARDET_ENCODING_HZ_GB_2312 = 'HZ-GB-2312';
  CHARDET_ENCODING_X_ISO_10646_UCS_4_3412 = 'X-ISO-10646-UCS-4-3412';
  CHARDET_ENCODING_X_ISO_10646_UCS_4_2143 = 'X-ISO-10646-UCS-4-2143';

  // Unused
  CHARDET_ENCODING_ISO_8859_2 = 'ISO-8859-2';
  CHARDET_ENCODING_WINDOWS_1250 = 'WINDOWS-1250';
  CHARDET_ENCODING_TIS_620 = 'TIS-620';

function chardet_create(var pdet: chardet_t): integer; stdcall;
  external 'universalchardet.dll' name '_chardet_create';

procedure chardet_destroy(det: chardet_t); stdcall;
  external 'universalchardet.dll' name '_chardet_destroy';

function chardet_handle_data(det: chardet_t; const data: PAnsiChar;
  len: Cardinal): integer; stdcall;
  external 'universalchardet.dll' name '_chardet_handle_data';

function chardet_data_end(det: chardet_t): integer; stdcall;
  external 'universalchardet.dll' name '_chardet_data_end';

function chardet_reset(det: chardet_t): integer; stdcall;
  external 'universalchardet.dll' name '_chardet_reset';

function chardet_get_charset(det: chardet_t; namebuf: PAnsiChar;
  buflen: Cardinal): integer; stdcall;
  external 'universalchardet.dll' name '_chardet_get_charset';

implementation

end.

このファイルをuniversalchardet.pasとして保存する。
実際に使用する場合は、users節にuniversalchardetを追加する。サンプルコードは以下の通り。

procedure TForm1.Button1Click(Sender: TObject);
var
  ms: TMemoryStream;
  enc: TEncoding;
  encname: array[0..CHARDET_MAX_ENCODING_NAME] of AnsiChar;
  det: chardet_t;
  res: Integer;
begin
  ms := TMemoryStream.Create;
  // ファイルのロード
  ms.LoadFromFile(filename);
  // オフセットをストリームの先頭に
  ms.Position := 0;

  // エンコーディングの判別をする
  det := nil;
  chardet_create(det);
  res := chardet_handle_data(det, ms.Memory, ms.Size);
  chardet_data_end(det);

  // エンコーディング名の取得
  chardet_get_charset(det, encname, CHARDET_MAX_ENCODING_NAME);
  chardet_destroy(det);

  // 自動判別結果を元にエンコーディングを判別して読み込み
  enc := nil;
  try
    enc := TEncoding.GetEncoding(encname);
  except
    on EEncodingError do
      enc := TEncoding.Default;
  end;

  Memo1.Lines.LoadFromStream(ms, enc);
  ms.Free;
end;

TBalloonHint::ShowHintの使い方

TBalloonHint::ShowHintに問題があるみたいで、代替にJVCLのTJvBalloonHintを使うというエントリーを書いたのだけれども、Embarcaderoの高橋さんからコメントがあって、再テスト。自分の勘違いで問題が無いことが確認できたのだけれども、以下の点に注意。

  • TBalloonHintは動的に生成せずにフォームに貼ったコンポーネントを使い回すこと。
  • ShowHintメソッドで指定する座標はフォームの座標ではなく、スクリーン座標を指定する。
  • ShowHintメソッドはヒント表示後すぐに終了する。HideAfterプロパティで指定した時間を待たないので、直後にdeleteなどで破棄するとヒントそのものが表示されない。

以下は正しく動作するコード例。TBalloonHintはコンポーネントとしてフォームに貼ること。

C++Builderの例:

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  BalloonHint1->Title = _T("ヒントのタイトル");
  BalloonHint1->Description = _T("バルーンヒントを表示してみる。");
  BalloonHint1->HideAfter = 2000;

  BalloonHint1->ShowHint(Button1->ClientToScreen(Button1->ClientRect.CenterPoint()));
}

Delphiの例:

procedure TForm1.Button1Click(Sender: TObject);
begin
  BalloonHint1.Title := 'ヒントのタイトル';
  BalloonHint1.Description := 'バルーンヒントを表示してみる。';
  BalloonHint1.HideAfter := 1500;

  BalloonHint1.ShowHint(Button1.ClientToScreen(CenterPoint(Button1.ClientRect)));
end;

でも、TBalloonHint::ShowHintのヘルプがないんだよな…。

Open Tools API その4:続・ソースエディタへのアクセス ソースエディタの「中身」をいじる

エディタの編集バッファ

ビュー(IOTAEditView)のBufferプロパティがソースエディタ内部のバッファで、ソースファイルの中身を編集する。インターフェースはITOAEditBuffer。
ソースエディタが1つのファイルをオープンしている場合、ビューは複数あっても、バッファは同一のを参照する。ソースエディタ(IOTASourceEditor)、ビュー(IOTAEditView)、バッファ(ITOAEditBuffer)の関係はこんな感じ。

        ┌ ビュー[0] ┐
        ├ ビュー[1] ┤
ソースエディタ ┼ ビュー[2] ┼ バッファ
        │   …   │
        └ ビュー[n] ┘

バッファのEditBlockプロパティがエディタで選択されている範囲。以下は、エディタの選択部分の位置と内容を取得する例。

function GetCurrentSourceEditor: IOTASourceEditor;
var
  i: Integer;
  Editor: IOTAEditor;
  ISourceEditor: IOTASourceEditor;
  CurrentModule: IOTAModule;
begin
  Result := nil;
  CurrentModule := (BorlandIDEServices as IOTAModuleServices).CurrentModule;

  for i := 0 to CurrentModule.GetModuleFileCount - 1 do
  begin
    Editor := CurrentModule.GetModuleFileEditor(i);

    if Supports(Editor, IOTASourceEditor, ISourceEditor) then
    begin
      Result := ISourceEditor;
      Break;
    end;
  end;
end

procedure TfrmEditorStatus.Button2Click(Sender: TObject);
var
  SourceEditor: IOTASourceEditor;
  EditBuffer: IOTAEditBuffer;
  Start, After: TOTACharPos;
  BlockType: TOTABlockType;
begin
  SourceEditor := GetCurrentSourceEditor;  // 現在アクティブなエディタを取得(GExpters由来)
  if SourceEditor = nil then
    Exit;
  Start := SourceEditor.BlockStart;  // 選択範囲の開始位置
  After := SourceEditor.BlockAfter;  // 選択範囲の終了位置

  // 現在編集中のファイルのバッファを取得
  EditBuffer := (BorlandIDEServices as IOTAEditorServices).TopBuffer;
  if SourceEditor = nil then
    Exit;

  // 選択範囲の内容を取得
  Memo1.Clear;
  Memo1.Text := EditBuffer.EditBlock.Text;
end;

バッファへ直接アクセスする

セレクションではなく、バッファを直接アクセスする場合は、編集リーダー(IOTAEditReader)と編集ライター(IOTAEditWriter)を介してアクセスする。バッファはUTF-8で管理しているので、UnicodeStringからは明示的に変換してやらないと文字化けするので注意。
バッファの中身を読み取る手順は、エディタのCreateReaderメソッドで編集リーダーを生成し、GetTextメソッドで中身を取得する。
以下は、GExpertsのユーティリティ関数で、エディタのバッファからTStreamへ中身を読み込むもの。

// 編集リーダーからTStreamへ読み込む
procedure GxSaveReaderToStream(EditReader: IOTAEditReader; Stream: TStream;
  TrailingNull: Boolean);
const
  // Leave typed constant as is - needed for streaming code.
  TerminatingNullChar: AnsiChar = #0;
var
  EditReaderPos: Integer;
  ReadDataSize: Integer;
  Buffer: array [0 .. EditReaderBufferSize] of AnsiChar;
  // Array of bytes, might be UTF-8
begin
  Assert(EditReader <> nil);
  Assert(Stream <> nil);

  EditReaderPos := 0;
  ReadDataSize := EditReader.GetText(EditReaderPos, Buffer,
    EditReaderBufferSize);
  Inc(EditReaderPos, ReadDataSize);
  while ReadDataSize = EditReaderBufferSize do
  begin
    Stream.Write(Buffer, ReadDataSize);
    ReadDataSize := EditReader.GetText(EditReaderPos, Buffer,
      EditReaderBufferSize);
    Inc(EditReaderPos, ReadDataSize);
  end;
  Stream.Write(Buffer, ReadDataSize);
  if TrailingNull then
    Stream.Write(TerminatingNullChar, SizeOf(TerminatingNullChar));
  // The source parsers need this
end;

// ソースエディタの中身をTSTringsにロードする
procedure GxLoadSourceEditorToUnicodeStrings(SourceEditor: IOTASourceEditor; Data: TStrings);
var
  MemStream: TMemoryStream;
begin
  Data.Clear;
  if not Assigned(SourceEditor) then
    raise Exception.Create
      ('No source editor in GxOtaLoadSourceEditorToUnicodeStrings');
  // TODO: Check stream format for forms as text (Ansi with escaped unicode, or UTF-8) in Delphi 2007/2009
  MemStream := TMemoryStream.Create;
  try
    GxSaveReaderToStream(SourceEditor.CreateReader, MemStream, False);
    MemStream.Position := 0;
{$IFDEF UNICODE}
    Data.LoadFromStream(MemStream, TEncoding.UTF8);
{$ELSE}
    if RunningDelphi8OrGreater then
      SynUnicode.LoadFromStream(Data, MemStream, seUTF8)
    else
      SynUnicode.LoadFromStream(Data, MemStream, seAnsi);
{$ENDIF}
  finally
    FreeAndNil(MemStream);
  end;
end;

どうも、CreateReaderで生成した編集リーダーは明示的に破棄する必要はなさそう。
一方、書き込む場合はIOTASourceEditorのCreateUndoableWriter*1メソッドで編集ライターを生成して、DeleteToメソッドで中身を削除したり、Insertメソッドで挿入する。
これまた、GExpertsのコードの一部。この関数は、エディタの中身を文字列で丸々入れ替える例。

procedure GxReplaceEditorText(SourceEditor: IOTASourceEditor; Text: string);
var
  Writer: IOTAEditWriter;
begin
  Assert(Assigned(SourceEditor));
  Writer := SourceEditor.CreateUndoableWriter;
  if not Assigned(Writer) then
    raise Exception.Create('No edit writer');
  Writer.DeleteTo(MaxLongint);
  Writer.Insert(PAnsiChar(AnsiToUtf8(Text)));
  Writer := nil;
end;

文字列をAnsiToUtf8関数で明示的にUTF-8へ変換して、そのポインタを渡している。編集ライターも明示的に破棄する必要は無いみたい。

まとめ

これで、Open Tools APIについてのネタは終わり。他にもOpen Tools APIはプロパティエディタやフォームエディタをカスタマイズしたり、キーバインドも変更できるので、アイデア次第でいろいろできそう。
DelphiC++BuilderIDEVCLで構築されているので、普通にアプリを作る上でのテクニックが十分に使用可能。資料は少ないけどGExpertsのソースコードとToolsAPI.pasはかなり参考になる。試行錯誤の末なので間違いがあるかもしれないので、その場合はアドバイスください。<(_ _)>

*1:CreateWriterメソッドもあるけど、こちらはUNDO不可な編集ライターを生成。

Open Tools API その3:ソースエディタへのアクセス

モジュール

モジュールとはIDEがアクセスする抽象的なエディタの組み合わせのことで、Delphi/C++Builderのプロジェクトにおけるユニットに相当。ユニットがソースファイル(*.pas/*.cpp)やヘッダファイル(*.h)、フォームファイル(*.dfm)の組み合わせであるように、モジュールは1つ以上のテキストエディタとフォームエディタなどから構成される。
モジュールに対するインターフェースを提供するのがIOTAModuleで、IDEからはIOTAModuleServiceより取得します。以下は、IDEが参照しているファイルの一覧を列挙する例。

procedure TfrmEditorStatus.Button6Click(Sender: TObject);
var
  I, J: Integer;
  ModuleServices: IOTAModuleServices;
  Editor: IOTAEditor;
  SourceEditor: IOTASourceEditor;
  FormEditor: IOTAFormEditor;
begin
  // モジュールの取得
  ModuleServices := (BorlandIDEServices as IOTAModuleServices);

  with ModuleServices do
  begin
    // IDEが開いているモジュールの列挙
    for I := 0 to ModuleCount - 1 do
    begin
      Memo1.Lines.Add(Modules[I].FileName);

      // モジュールが参照するエディタの列挙
      for J := 0 to Modules[I].GetModuleFileCount - 1 do
      begin
        Editor := Modules[I].GetModuleFileEditor(J);
        if Supports(Editor, IOTASourceEditor, SourceEditor) then
        begin
          // エディタはソースエディタ
          Memo1.Lines.Add('SourceEditor - ' + SourceEditor.FileName);
        end
        else if Supports(Editor, IOTAFormEditor, FormEditor) then
        begin
          // エディタはフォームエディタ
          Memo1.Lines.Add('FormEditor - ' + FormEditor.FileName);
        end else
        begin
          // 不明
          Memo1.Lines.Add('Unknown - ' + Editor.FileName);
        end;
      end;
    end;
  end;
end;

エディタの種類の判別には、Supports関数でそれぞれのインターフェースをサポートするかどうかで判別する。

ソースエディタ、ビュー

ソースエディタのインターフェースがIOTASourceEditorで、ソースエディタは複数個のビュー(IOTAEditView)から構成される。EditViewCountプロパティがビューの個数で、この値が0の場合はサブビューが隠れていることを表しす。ビューそのものはEditViews[n]でアクセスする。
ビューがエディタそのもので、カーソル位置やセレクション情報、内部バッファなどの情報をもつ。
IOTAEditViewの主なプロパティ

プロパティ 型名 内容
Block IOTAEditBlock ブロック:エディタの選択範囲
Buffer IOTABuffer バッファ:エディタの内部バッファ
Position IOTAEditPosition カーソル位置
TopRow Integer 最上部に表示されている行の行番号
RightColumn Integer ビューの右端の桁番号

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);