Configuring RichTextCtrl to handle URLs

I have been trying to set up styles for URLs in a RichTextCtrl so that they appear as blue, underlined text and when you left-click on them they will trigger an EVT_TEXT_URL event.

That part is working. However, if I click on one of the URLs and then load new text (or even the same text) into the RTC, all the text is marked as URLs!

I have only tested this on wxPython 4.2.3 gtk3 (phoenix) wxWidgets 3.2.7 + Python 3.12.3 + Linux Mint 22.2, so I don’t know if it happens on other platforms.

Below is a simplified example. To trigger the behaviour, left-click on one of the URLs and then click on the “Reload” button.

Any ideas for how to stop this happening?

import re
import wx
import wx.richtext as rt

TEXT = """\
Here are some URLs to test.

Left-click on a URL, then click on "Reload" 
and *all* the text will be marked as URLs????

 https://wxpython.org/

 https://www.bbc.co.uk/news

 https://www.theregister.com/Week/

"""

URL_REGEX = re.compile(r'(http|https)://([\w\-_]+(?:\.[\w\-_]+)+)([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?')

class MyFrame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent)
        self.SetSize(wx.Size(600, 350))
        self.SetTitle("Test RichTextCtrl")
        self.panel = wx.Panel(self, wx.ID_ANY)
        main_sizer = wx.BoxSizer(wx.VERTICAL)
        self.rtc = rt.RichTextCtrl(self.panel, wx.ID_ANY)
        main_sizer.Add(self.rtc, 1, wx.EXPAND, 0)
        bottom_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.reload_button = wx.Button(self.panel, wx.ID_ANY, "Reload")
        bottom_sizer.Add(self.reload_button, 0, wx.RIGHT, 16)
        self.close_button = wx.Button(self.panel, wx.ID_ANY, "Close")
        bottom_sizer.Add(self.close_button, 0, 0, 0)
        main_sizer.Add(bottom_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.TOP | wx.BOTTOM, 8)
        self.panel.SetSizer(main_sizer)
        self.Layout()

        self.displayText()

        self.rtc.Bind(wx.EVT_TEXT_URL, self.OnURL)
        self.reload_button.Bind(wx.EVT_BUTTON, self.OnReload)
        self.close_button.Bind(wx.EVT_BUTTON, self.OnClose)

    def displayText(self):
        print("Display Text:")
        self.rtc.SetValue(TEXT)
        self.markAllURLs(TEXT)

    def markURL(self, start, end, url_text):
        """Mark a URL in the RichTextCtrl. """

        url_style = rt.RichTextAttr()
        url_style.SetTextColour(wx.BLUE)
        url_style.SetFontUnderlined(True)
        url_style.SetURL(url_text)
        self.rtc.SetStyle(start, end, url_style)

    def markAllURLs(self, text):
        """Mark all the URLs in the RichTextCtrl. """

        for m in re.finditer(URL_REGEX, text):
            print(f" {m.start(0)} {m.end(0)} {m.group(0)}")
            self.markURL(m.start(0), m.end(0), m.group(0))

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

    def OnReload(self, _evt):
        self.displayText()

    def OnURL(self, evt):
        print("OnURL()", evt.GetString())

if __name__ == "__main__":
    app = wx.App()
    frame = MyFrame(None)
    frame.Show()
    app.MainLoop()

I have found that, if I set the whole text to the basic style before marking the URLs, it no longer marks the whole text as URLs.

I modified the displayText() method like this:

    def displayText(self):
        print("Display Text:")
        self.rtc.SetValue(TEXT)
        self.rtc.SetStyle(0, self.rtc.GetLastPosition(), self.rtc.GetBasicStyle())
        self.markAllURLs(TEXT)

EDIT: I’m not sure if that’s a real fix or a just a workaround for a bug in wxPython or in my original code.