How to invoke a method when a control has finished processing events?

Hi,

I am using Python 3.7.9 and wxPython 4.1.1. on Fedora 32.

I have the following problem:
I would like to run a method when the RichTextCtrl in the example below has finished processing a keyboard input and the text is changed.
There are three events that I might use but none of them do what I want.

EVT_TEXT almost works, but for some reason does not fire when the bullet of a list is deleted.
You can try it in the example, position the caret at the beginning of a list item and press backspace. Nothing happens, if it worked you would see Example list item 3 TEXT EVENT in the console.

EVT_KEY_UP is only fired when you stop holding a key. But I need to register all changes to the text.

EVT_KEY_DOWN is fired before the control processes the event and thus the text has not changed yet.

My use case for this is that I need to do modifications to the paragraph styles when certain changes happen. For example I want to change the style of the list item when the bullet is deleted to normal paragraph because it no longer is a list item.
Or when two paragraphs are joined together by pressing backspace or delete key I want to change the resulting combined paragraph into one style.

Is there any way how to hook into the RichTextCtrl and run a method when the text changes including corner cases like list items?

Thank you.

import wx
import wx.richtext as rt


class RichTextFrame(wx.Frame):
    def __init__(self, *args, **kw):
        wx.Frame.__init__(self, *args, **kw)
        self.rtc = rt.RichTextCtrl(self, style=wx.VSCROLL | wx.HSCROLL | wx.NO_BORDER)
        self.sizer = wx.BoxSizer(wx.VERTICAL)

        # Create style stylesheet and control
        self._stylesheet = rt.RichTextStyleSheet()
        self._stylesheet.SetName('Stylesheet')

        self.rtc.SetStyleSheet(self._stylesheet)
        self.sizer.Add(self.rtc, 1, flag=wx.EXPAND)
        self.SetSizer(self.sizer)

        self.rtc.Bind(wx.EVT_TEXT, self._on_text_handler)
        self.rtc.Bind(wx.EVT_KEY_UP, self._on_key_up_handler)
        self.rtc.Bind(wx.EVT_KEY_DOWN, self._on_key_down_handler)

        self._create_styles()
        self._insert_sample_text()

    def _on_text_handler(self, evt: wx.CommandEvent) -> None:
        """
        Handle text events.
        :param evt: Not used
        :return: None
        """
        evt.Skip()
        position = self.rtc.GetAdjustedCaretPosition(self.rtc.GetCaretPosition())
        p: rt.RichTextParagraph = self.rtc.GetFocusObject().GetParagraphAtPosition(position)
        print(p.GetTextForRange(p.GetRange()))

    def _on_key_up_handler(self, evt: wx.CommandEvent) -> None:
        """
        Handle text events.
        :param evt: Not used
        :return: None
        """
        evt.Skip()
        position = self.rtc.GetAdjustedCaretPosition(self.rtc.GetCaretPosition())
        p: rt.RichTextParagraph = self.rtc.GetFocusObject().GetParagraphAtPosition(position)
        print(p.GetTextForRange(p.GetRange()) + ' KEY UP')

    def _on_key_down_handler(self, evt: wx.CommandEvent) -> None:
        """
        Handle text events.
        :param evt: Not used
        :return: None
        """
        evt.Skip()
        position = self.rtc.GetAdjustedCaretPosition(self.rtc.GetCaretPosition())
        p: rt.RichTextParagraph = self.rtc.GetFocusObject().GetParagraphAtPosition(position)
        print(p.GetTextForRange(p.GetRange()) + ' KEY DOWN')

    def _create_styles(self) -> None:
        """
        Create styles for rich text control.
        :return: None
        """
        # List style
        stl_list: rt.RichTextAttr = rt.RichTextAttr()
        stl_list.SetFontSize(12)
        stl_list.SetAlignment(wx.TEXT_ALIGNMENT_LEFT)
        stl_list.SetFontWeight(wx.FONTWEIGHT_NORMAL)
        stl_list.SetParagraphSpacingBefore(20)
        stl_list.SetParagraphSpacingAfter(20)
        stl_list.SetBackgroundColour(wx.GREEN)

        stl_list_1: rt.RichTextAttr = rt.RichTextAttr()
        stl_list_1.SetFontSize(12)
        stl_list_1.SetAlignment(wx.TEXT_ALIGNMENT_LEFT)
        stl_list_1.SetFontWeight(wx.FONTWEIGHT_NORMAL)
        stl_list_1.SetBulletStyle(wx.TEXT_ATTR_BULLET_STYLE_STANDARD)
        stl_list_1.SetLeftIndent(20, 40)

        style_list: rt.RichTextListStyleDefinition = rt.RichTextListStyleDefinition('list')
        style_list.SetLevelAttributes(0, stl_list_1)
        style_list.SetStyle(stl_list)
        style_list.SetNextStyle('list')
        self._stylesheet.AddListStyle(style_list)

        self.rtc.SetStyleSheet(self._stylesheet)

    def _insert_sample_text(self) -> None:
        """
        Insert sample text.
        :return: None
        """
        list_style = self._stylesheet.FindListStyle('list').GetCombinedStyleForLevel(0)
        self.rtc.BeginStyle(list_style)
        self.rtc.WriteText('Example list item 1')
        self.rtc.Newline()
        self.rtc.WriteText('Example list item 2')
        self.rtc.Newline()
        self.rtc.WriteText('Example list item 3')
        self.rtc.Newline()
        self.rtc.EndStyle()


class MyApp(wx.App):
    """
    Main class for running the gui
    """

    def __init__(self):
        wx.App.__init__(self)
        self.frame = None

    def OnInit(self):
        self.frame = RichTextFrame(None, -1, "RichTextCtrl", size=(900, 700), style=wx.DEFAULT_FRAME_STYLE)
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True


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

Well, I haven’t done any RichText but I would guess EVT_TEXT + RichTextEvent class and some ingenuity

Can you please be a little bit more specific? As I wrote above:
EVT_TEXT almost works, but for some reason does not fire when the bullet of a list item is deleted. You can try it in the example code.

So what do you mean by your comment?
Thanks.

It’s pretty simple, your question is somewhat a contradiction; a control may put events into the queue to which your app may bind a method for doing some processing; so if the control you have picked doesn’t post an event you are interested in then there is no way to process what’s on your mind unless the control is suitably modified.
So what I’ve said was if those events I mentioned don’t fit then you have either not picked the proper control for what you want or you’ll have to reconsider your goal (or try to get an upgrade on that control, which may drag on)

Unfortunately even the:
EVT_RICHTEXT_CHARACTER
EVT_RICHTEXT_DELETE
EVT_RICHTEXT_CONTENT_DELETED
are not fired either when the bullet gets deleted.

But I did find a sort of a workaround.
I have a method bound to EVT_KEY_DOWN which is fired always and at the end of it it calls wx.CallAfter(self._print_paragraph) because I read that this method calls the function when the current event handler has exited. This appears to work but I do not have very good understanding of why it works.
I think it waits until the global event queue is empty, which means the control must have finished processing the event but at the same time I do not know what is the current event handler. The RichTextCtr is apparently an event handler of the RichTextBuffer but at the same time there is probably another global event handler and also the method that processes events is sometimes called an event handler.
If anyone could explain this a bit better I would be grateful for that.

Notice that with this when the bullet is deleted, the paragraph is printed out and if two paragraphs (lines) are joined together, the whole combined paragraph is printed.

Here is an example code that shows it in action:

import wx
import wx.richtext as rt


class RichTextFrame(wx.Frame):
    def __init__(self, *args, **kw):
        wx.Frame.__init__(self, *args, **kw)
        self.rtc = rt.RichTextCtrl(self, style=wx.VSCROLL | wx.HSCROLL | wx.NO_BORDER)
        self.sizer = wx.BoxSizer(wx.VERTICAL)

        # Create style stylesheet and control
        self._stylesheet = rt.RichTextStyleSheet()
        self._stylesheet.SetName('Stylesheet')

        self.rtc.SetStyleSheet(self._stylesheet)
        self.sizer.Add(self.rtc, 1, flag=wx.EXPAND)
        self.SetSizer(self.sizer)

        self.rtc.Bind(wx.EVT_KEY_DOWN, self._on_text_handler)

        self._create_styles()
        self._insert_sample_text()

    def _on_text_handler(self, evt: wx.CommandEvent) -> None:
        """
        Handle text events.
        :param evt: Not used
        :return: None
        """
        evt.Skip()
        print('KEY DOWN')
        wx.CallAfter(self._print_paragraph)

    def _print_paragraph(self) -> None:
        """
        Print current paragraph text.
        :return: None
        """
        position = self.rtc.GetAdjustedCaretPosition(self.rtc.GetCaretPosition())
        p: rt.RichTextParagraph = self.rtc.GetFocusObject().GetParagraphAtPosition(position)
        print(p.GetTextForRange(p.GetRange()))

    def _create_styles(self) -> None:
        """
        Create styles for rich text control.
        :return: None
        """
        # List style
        stl_list: rt.RichTextAttr = rt.RichTextAttr()
        stl_list.SetFontSize(12)
        stl_list.SetAlignment(wx.TEXT_ALIGNMENT_LEFT)
        stl_list.SetFontWeight(wx.FONTWEIGHT_NORMAL)
        stl_list.SetParagraphSpacingBefore(20)
        stl_list.SetParagraphSpacingAfter(20)
        stl_list.SetBackgroundColour(wx.GREEN)

        stl_list_1: rt.RichTextAttr = rt.RichTextAttr()
        stl_list_1.SetFontSize(12)
        stl_list_1.SetAlignment(wx.TEXT_ALIGNMENT_LEFT)
        stl_list_1.SetFontWeight(wx.FONTWEIGHT_NORMAL)
        stl_list_1.SetBulletStyle(wx.TEXT_ATTR_BULLET_STYLE_STANDARD)
        stl_list_1.SetLeftIndent(20, 40)

        style_list: rt.RichTextListStyleDefinition = rt.RichTextListStyleDefinition('list')
        style_list.SetLevelAttributes(0, stl_list_1)
        style_list.SetStyle(stl_list)
        style_list.SetNextStyle('list')
        self._stylesheet.AddListStyle(style_list)

        self.rtc.SetStyleSheet(self._stylesheet)

    def _insert_sample_text(self) -> None:
        """
        Insert sample text.
        :return: None
        """
        list_style = self._stylesheet.FindListStyle('list').GetCombinedStyleForLevel(0)
        self.rtc.BeginStyle(list_style)
        self.rtc.WriteText('Example list item 1')
        self.rtc.Newline()
        self.rtc.WriteText('Example list item 2')
        self.rtc.Newline()
        self.rtc.WriteText('Example list item 3')
        self.rtc.Newline()
        self.rtc.EndStyle()


class MyApp(wx.App):
    """
    Main class for running the gui
    """

    def __init__(self):
        wx.App.__init__(self)
        self.frame = None

    def OnInit(self):
        self.frame = RichTextFrame(None, -1, "RichTextCtrl", size=(900, 700), style=wx.DEFAULT_FRAME_STYLE)
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True


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

It adds an event to the pending event queue. When the primary event queue becomes empty then the pending events are processed. The handler for the call-after events then calls the given callable object passing any args that were passed to the wx.CallAfter.

This is similar to the way that EVT_IDLE events work, except they are sent each time the event queue becomes empty.

1 Like