Dynamic context menu for RichTextCtrl

Unlike the StyledTextCtrl, when you right-click in a RichTextCtrl, the caret is not automatically moved to the point in the text where you clicked before the context menu is displayed.

If I bind EVT_CONTEXT_MENU to a custom handler, I can get the coordinates of the point that was clicked from the event object and I can convert that position to client coordinates. However, I have not been able to find a way to convert those coordinates into a caret position.

The reason I want to be able to do this is so that I can dynamically add spelling options to the context menu before it is displayed. The options would depend on whether there was a spelling error at the point that was clicked and, if so, include a list of suggested corrections.

Can anyone suggest a way to get the position in the text?

I have included a simple application to demonstrate this issue.

import wx
import wx.richtext as rt

class MyFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        self.SetSize((400, 200))
        self.SetTitle("Context Menu")
        self.panel_1 = wx.Panel(self, wx.ID_ANY)
        sizer_1 = wx.BoxSizer(wx.VERTICAL)
        self.rtc = rt.RichTextCtrl(self.panel_1, wx.ID_ANY)
        sizer_1.Add(self.rtc, 1, wx.EXPAND, 0)
        sizer_2 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_1.Add(sizer_2, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM | wx.TOP, 8)
        self.close_button = wx.Button(self.panel_1, wx.ID_ANY, "Close")
        sizer_2.Add(self.close_button, 0, 0, 0)
        self.panel_1.SetSizer(sizer_1)
        self.Layout()
        self.Bind(wx.EVT_BUTTON, self.OnClose, self.close_button)
        self.rtc.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu)
        self.rtc.SetValue("01234567890")


    def OnClose(self, _event):
        self.Destroy()


    def OnContextMenu(self, event):
        cp = self.rtc.GetCaretPosition()
        ip = self.rtc.GetInsertionPoint()
        print("Caret: %d  Insertion: %d" % (cp, ip))

        context_menu = self.rtc.GetContextMenu()
        point = event.GetPosition()
        point = self.ScreenToClient(point)

        # How to access the text at the point that was clicked?
        pass

        self.rtc.PopupMenu(context_menu, point)


class MyApp(wx.App):
    def OnInit(self):
        self.frame = MyFrame(None, wx.ID_ANY, "")
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True


if __name__ == "__main__":
    app = MyApp(0)
    app.MainLoop()

Hi RichardT,

Maybe I got the correct result using,

        res, pos = self.rtc.HitTest(point)
        flag, x, y = self.rtc.PositionToXY(pos)

I’m not sure if it works when I change the font style or embed images…

1 Like

Hi Kazuya,

Thank you for the suggestion. I had previously looked at the HitTest() method but was confused by the “not adjusted for the client area origin nor for scrolling” comment in the documentation, thinking that meant that it couldn’t give the correct position value if the text had been scrolled. However, I have now modified my example to call it and then use the position value it returns to find the word at that point. It appears to be working fine, even with a mixture of styles and irrespective of how much the text has been scrolled.

Your reservation about embedded images is valid though, as they do cause my calculated start and end of the word to be wrong. My application has no requirement to support images, so I will attempt to prevent them being pasted into the control.

I also looked at the PositionToXY() method, but don’t know how I could use the values it returns when trying to examine the text in the control.