delphitoolsapi

Detect when the ide is fully loaded


my english is bad so i try to be short.
This fail when is in the constructor:

fNTAServices.AddActionMenu('ViewToolWindowsItem', nil, CustomMenuItem);

because the mainmenu is not fully created at the moment the addon constructor is called.
I didnt see any "ide ready" notifier in the toolsapi, so i'm using a IOTAIDENotifier that call the AddActionMenu (once) when the NotifyCode is ofnEndProjectGroupOpen.
There is a better way?

PS.
the addon is a dll not a package

EDIT:
"minimal reproducible example" test code below.
I have added a CreateAnonymousThread in the contructor to check when the 'ViewToolWindowsItem' spawn, the time is around 100 to 300+ ms on my pc.
EDIT 2:
I tried to printing out all the MainMenu names while in the constructor, some are there some not, i'm not sure if the ide window is in another thread or if some menu items are created on the ide constructor then load the plugins then add the others when the ide show.

library IDE_EXTENSION_DLL_TEST;

uses
  WinAPI.Windows,
  System.SysUtils, System.Classes, System.Diagnostics,
  VCL.Menus,
  ToolsAPI;

{$R *.res}

type
  TMenuTest = class(TInterfacedObject, IOTAWizard, IOTAMenuWizard)
    // IOTANotifier
    procedure AfterSave;
    procedure BeforeSave;
    procedure Destroyed;
    procedure Modified;
    // IOTAWizard
    function GetIDString: string;
    function GetName: string;
    function GetState: TWizardState;
    procedure Execute;
    // IOTAMenuWizard
    function GetMenuText: string;
  strict private
    fNTAServices: INTAServices;
    fCustomMenuItem: TMenuItem;
  public
    constructor Create;
  end;

  // IOTANotifier
procedure TMenuTest.AfterSave;
begin
  //
end;

procedure TMenuTest.BeforeSave;
begin
  //
end;

procedure TMenuTest.Destroyed;
begin
  //
end;

procedure TMenuTest.Modified;
begin
  //
end;

// IOTAWizard
function TMenuTest.GetIDString: string;
begin
  result := 'TMenuTest';
end;

function TMenuTest.GetName: string;
begin
  result := 'Add menu test plugin ';
end;

function TMenuTest.GetState: TWizardState;
begin
  result := [wsEnabled];
end;

procedure TMenuTest.Execute;
begin
  if Assigned(fNTAServices) then
    try
      { This work fine }
      fNTAServices.AddActionMenu('ViewToolWindowsItem', nil, fCustomMenuItem);
    except
      on e: exception do
        OutputDebugString(PChar('AddActionMenu (execute) ERR > ' + e.Message));
    end;
end;

// IOTAMenuWizard
function TMenuTest.GetMenuText: string;
begin
  { this is added in Help > Help Wizards and call TMenuTest.Execute }
  result := 'Add menu';
end;

constructor TMenuTest.Create;
begin
  inherited;
  fCustomMenuItem := TMenuItem.Create(nil); // free destr
  fCustomMenuItem.Caption := 'CustomMenuItem';
  fCustomMenuItem.Name := 'CustomMenuItemName';

  if Supports(BorlandIDEServices, INTAServices, fNTAServices) then
  begin
    OutputDebugString('Supports ok');
    try
      { This fail with "Cannot locate menu item 'ViewToolWindowsItem'" }
      fNTAServices.AddActionMenu('ViewToolWindowsItem', nil, fCustomMenuItem);
    except
      on e: exception do
        OutputDebugString(PChar('AddActionMenu (constructor) ERR > ' + e.Message));
    end;

    { This may be some bad code but it work >_> }
    TThread.CreateAnonymousThread(
      procedure
      var
        vOk: boolean;
        vSW: TStopWatch;
      begin
        vSW := TStopWatch.Create;
        vSW.Start;
        repeat
          vOk := true;
          try
            fNTAServices.AddActionMenu('ViewToolWindowsItem', nil, fCustomMenuItem);
          except
            on e: exception do
            begin
              vOk := false;
              OutputDebugString(PChar('AddActionMenu (constructor) ERR > ' + e.Message));
            end;
          end;
        until vOk;
        vSW.Stop;
        // i get random 100/300+ ms with a 13th gen i5
        OutputDebugString(PChar('vOk True > ' + vSW.Elapsed.Milliseconds.ToString));
      end).Start;

  end
  else
    OutputDebugString('Supports fail');
end;

// INITWIZARD0001
function InitWizard(const aBorlandIDEServices: IBorlandIDEServices; aWizardRegisterProc: TWizardRegisterProc; var aWizardTerminateProc: TWizardTerminateProc): boolean; stdcall;
const
  cBoolStr: array [boolean] of string = ('False', 'True');
begin
  if Assigned(aBorlandIDEServices) then
  begin
    aWizardRegisterProc(TMenuTest.Create);
    result := true;
  end
  else
    result := false;
  OutputDebugString(PChar(Format('InitWizard %s', [cBoolStr[result]])));
end;

exports
  InitWizard name WizardEntryPoint;

begin
  OutputDebugString('dll attach');

end.

Solution

  • This is the way GExperts handles this:

    constructor TGExperts.Create;
    begin
      inherited Create;
      FStartingUp := True;
      InitializeGExperts;
    
      // No idea where these 1200 ms came from, but
      // basically it means that the method will be
      // called when the application handles events
      // the first time.
      TTimedCallback.Create(DoAfterIDEInitialized, 1200, True);
    end;
    

    TTimedCallback.Create creates a TTimer and adds code to its OnTimer event that calls DoAfterIDEInitialized and after that frees the timer.

    I don't know whether this is the best solution, I just inherited that code from previous developers (that comment about 1200 ms is mine). All I can say is that it works.