Adding Menu Items to the IDE Editor’s Context Menu

By | November 25, 2019

I decided not so long ago to create a new IDE plug-in to consolidate a few separate tools that are related to debugging. While doing this I found (as others had) that changes in the way the IDE handles the context menu in the editor stopped my code being able to insert context menu items.

I believe this is because the IDE context menu is now dynamic and not static and gets created every time it’s required rather than once at the IDE start-up.

So the following is how I solved the problem and it starts with a Timer (no eggs were hurt in the experiment). I generally create DLL plug-ins and I found that the Editor and its context menu are not available when my plug-in is initialised so I need to wait until they are available and then do something. Below is the timer code (note, all this code is in my plug-in wizard).

Procedure TDDTWizard.MenuInstallerTimer(Sender: TObject);

Begin
  HookEditorPopupMenu;
End; 

Nothing outrageous here, it just calls a method to hook the editor pop-up menu, the name should hint to you what I’m about to do but no peeking.

The timer is simply started from the constructor for the wizard as follows:

Constructor TDDTWizard.Create;

Const
   iTimerInterval = 1000;

Begin
  Inherited Create;
  ...
  FEditorPopupMethod.Data := Nil;
  FMenuInstalled := False;
  FMenuTimer := TTimer.Create(Nil);
  FMenuTimer.Interval := iTimerInterval;
  FMenuTimer.OnTimer := MenuInstallerTimer;
  FMenuTimer.Enabled := True;
End;

Again nothing unusual here except for the initialisation of the FEditorPopupMethod.Data member. FEditorPopupMethod is declared as a TMethod and we are going to use it to hold a reference to an existing editor pop-up menu event handlers.

So now for the interesting bit – what’s in the HookEditorPopupMenu method.

Procedure TDDTWizard.HookEditorPopupMenu;

Var
  EditorPopupMenu : TPopupActionBar;

Begin
  EditorPopupMenu := FindEditorPopup;
  If Assigned(EditorPopupMenu) Then
    Begin
      FImageIndex := AddImageToList(EditorPopupMenu.Images);
      If Assigned(EditorPopupMenu.OnPopup) Then
        Begin
          FEditorPopupMethod := TMethod(EditorPopupMenu.OnPopup);
          EditorPopupMenu.OnPopup := DebuggingToolsPopupEvent;
        End Else
          EditorPopupMenu.OnPopup := DebuggingToolsPopupEvent;
      FMenuTimer.Enabled := False;
    End;
End; 

The above code attempts to find the Editor Popup Menu using a custom function FindEditorPopup() which we’ll talk about in a moment. If it gets a valid reference then an image is installed into the Popup Menu’s image list and then one or two things can happen.

If the Editor Popup Menu already has an event handler (either because the IDE’s set one or another plug-in did) then we need to store this and install our own else we just install our own.

Once we’ve done this then we can disabled the timer as we only need to hook this once.

So what does FindEditorPopup() do? Let’s see…

Function  TDDTWizard.FindEditorPopup : TPopupActionBar;

Const
  strEditorLocalMenuComponentName = 'EditorLocalMenu';

Var
  EditorForm: TForm;

Begin
  Result := Nil;
  EditorForm := FindEditWindow;
  If Assigned(EditorForm) Then
    Begin
      Result := FindComponent(
        EditorForm,
        strEditorLocalMenuComponentName,
        TPopupActionBar
      ) As TPopupActionBar;
    End;
End;

This function delegates to two other functions to find the Editor window and the Popup Menu component and they are implemented as follows:

Function TDDTWizard.FindEditWindow : TForm;

Const
  strTEditWindowClassName = 'TEditWindow';

Var
  iForm: Integer;

Begin
  Result := Nil;
  For iForm := 0 To Screen.FormCount - 1 Do
    If CompareText(Screen.Forms[iForm].ClassName, strTEditWindowClassName) = 0 Then
      Begin
        Result := Screen.Forms[iForm];
        Break;
      End;
End;

This method searches for the TEditWindow class name in the IDE’s list of forms and returns its reference if found.

Function TDDTWizard.FindComponent(Const OwnerComponent : TComponent; Const strName : String; Const ClsType : TClass) : TComponent;

Var
  iComponent: Integer;

Begin
  Result := Nil;
  For iComponent := 0 To OwnerComponent.ComponentCount - 1 Do
    If CompareText(OwnerComponent.Components[iComponent].Name, strName) = 0 Then
      If OwnerComponent.Components[iComponent] Is ClsType Then
        Begin
          Result := OwnerComponent.Components[iComponent];
          Break;
        End;
End;

The above searches the given component for the given component name with the given class type, i.e. we call this looking for the Popup Menu.

While we’re at it we should say what happens in the AddImageToList method. Here we extract a bitmap from the DLL’s resources and add it to the image list associated with the Popup Menu.

Function TDDTWizard.AddImageToList(Const ImageList : TCustomImageList): Integer;

Const
  strImageName = 'DDTMenuBitMap16x16';

Var
  BM : VCL.Graphics.TBitMap;

Begin
  Result := -1;
  If FindResource(hInstance, strImageName, RT_BITMAP) > 0 Then
    Begin
      BM := VCL.Graphics.TBitMap.Create;
      Try
        BM.LoadFromResourceName(hInstance, strImageName);
        Result := ImageList.AddMasked(BM, clLime);
      Finally
        BM.Free;
      End;
    End;
End;

So next we need to look at what the event handler we’ve installed does to actually add out menu items.

Procedure TDDTWizard.DebuggingToolsPopupEvent(Sender: TObject);

ResourceString
  strDebuggingTools = 'Debugging Tools';
  strAddBreakpoint = 'Add Breakpoint';
  strDebugWithCodeSiteCaption = 'Debug &with CodeSite';

Var
  NotifyEvent : TNotifyEvent;
  EditorPopupMenu: TPopupActionBar;
  MI: TMenuItem;

Begin
  If Assigned(FEditorPopupMethod.Data) Then
    Begin
      NotifyEvent := TNotifyEvent(FEditorPopupMethod);
      NotifyEvent(Sender);
    End;
  EditorPopupMenu := FindEditorPopup;
  If Assigned(EditorPopupMenu) Then
    Begin
      If Assigned(FDebuggingToolsMenu) Then
        FDebuggingToolsMenu.Free;
      // Create Main Menu Item
      FDebuggingToolsMenu := TMenuItem.Create(EditorPopupMenu);
      FDebuggingToolsMenu.Caption := strDebuggingTools;
      //FDebuggingToolsMenu.OnClick := DebugWithCodeSite;
      FDebuggingToolsMenu.ImageIndex := FImageIndex;
      EditorPopupMenu.Items.Add(FDebuggingToolsMenu);
      // Create Add Breapoint
      MI := TMenuItem.Create(FDebuggingToolsMenu);
      MI.Caption := strAddBreakpoint;
      MI.OnClick := AddBreakpoint;
      MI.ImageIndex := FImageIndex;
      FDebuggingToolsMenu.Add(MI);
      // Create Debug with CodeSite
      MI := TMenuItem.Create(FDebuggingToolsMenu);
      MI.Caption := strDebugWithCodeSiteCaption;
      MI.OnClick := DebugWithCodeSite;
      MI.ImageIndex := FImageIndex;
      FDebuggingToolsMenu.Add(MI);
    End;
End; 

This first thing we do is see if we had a previous event handler for the Popup Menu and if so we call it to ensure that either the IDE’s code or another plug-in’s code is run.

Once we have done this we get a reference to the Editor Popup Menu and add three menu items, the first a parent for the other two cascading menus and we hook these to TNotifyEvent methods which do the actual work we require.

That’s it, all done, or is it…

Well for a DLL yes as you would never get a situation where the Editor Popup Menu is called when you DLL is not in memory any more however this is not true for a BPL based plug-in so we need to reverse our hook. This starts in the Wizard’s destructor as follows:

Destructor TDDTWizard.Destroy;

Begin
  UnhookEditorPopupMenu;
  ...
  FMenuTimer.Free;
  Inherited Destroy;
End; 

This in turn calls the code to unhook the menu event as follows:

Procedure TDDTWizard.UnhookEditorPopupMenu;

Var
  EditorPopupMenu : TPopupActionBar;

Begin
  {$IFDEF DEBUG} CodeSite.TraceMethod(Self, 'UnhookEditorPopupMenu', tmoTiming); {$ENDIF DEBUG}
  EditorPopupMenu := FindEditorPopup;
  If Assigned(EditorPopupMenu) And Assigned(EditorPopupMenu.OnPopup) Then
    EditorPopupMenu.OnPopup := TNotifyEvent(FEditorPopupMethod);
End; 

This simply finds the Editor Popup Menu and re-assigns the event handler we stored in the FEditorPopupMenu reference.

Knowing there are changes in how this is implemented in different IDEs I’ve test this code back to XE8 and all seems to work as expected.

Enjoy D.