RichTextCtrl changing style does not replace previous style

Hi,

I need some help manipulating styles in text. The example below can be used to demonstrate the issue.
I am using python3 and wxPython 4.1.1.

When you run the script it inserts 3 paragraphs into the text field. Click on one of the red lines and then apply the blue style from the style picker at the bottom. Notice that it sort of adds the style to the already existing style and places it over.
But in the code, each style redefines all the properties of the style.

Also notice that if you start a new paragraph yourself with a style, switching the styles then does not behave the same way.

Why? What is this? What is happening? :smiley:

Thanks.

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._style_control = rt.RichTextStyleListBox(self, 1, size=(140, 60))
        self._style_control.SetStyleType(0)
        self._style_control.SetMargins(-5, -5)

        self.rtc.SetStyleSheet(self._stylesheet)
        self._style_control.SetRichTextCtrl(self.rtc)
        self._style_control.SetStyleSheet(self._stylesheet)

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

        self._create_styles()
        self._insert_sample_text()

    def _create_styles(self) -> None:
        """
        Create styles for rich text control.
        :return: None
        """
        # Paragraph style
        stl_paragraph: rt.RichTextAttr = rt.RichTextAttr()
        stl_paragraph.SetFontSize(Numbers.paragraph_font_size)
        stl_paragraph.SetAlignment(wx.TEXT_ALIGNMENT_LEFT)
        stl_paragraph.SetFontWeight(wx.FONTWEIGHT_NORMAL)
        stl_paragraph.SetParagraphSpacingBefore(Numbers.paragraph_spacing)
        stl_paragraph.SetParagraphSpacingAfter(Numbers.paragraph_spacing)
        stl_paragraph.SetBackgroundColour(wx.RED)

        style_paragraph: rt.RichTextParagraphStyleDefinition = rt.RichTextParagraphStyleDefinition(
            Strings.style_paragraph)
        style_paragraph.SetStyle(stl_paragraph)
        style_paragraph.SetNextStyle(Strings.style_paragraph)
        self._stylesheet.AddParagraphStyle(style_paragraph)

        # Paragraph style bold
        stl_paragraph_bold: rt.RichTextAttr = rt.RichTextAttr()
        stl_paragraph_bold.SetFontSize(Numbers.paragraph_font_size_1)
        stl_paragraph_bold.SetAlignment(wx.TEXT_ALIGNMENT_LEFT)
        stl_paragraph_bold.SetFontWeight(wx.BOLD)
        stl_paragraph_bold.SetParagraphSpacingBefore(Numbers.paragraph_spacing)
        stl_paragraph_bold.SetParagraphSpacingAfter(Numbers.paragraph_spacing)
        stl_paragraph_bold.SetBackgroundColour(wx.BLUE)

        style_paragraph_bold: rt.RichTextParagraphStyleDefinition = rt.RichTextParagraphStyleDefinition(
            Strings.style_paragraph_bold)
        style_paragraph_bold.SetStyle(stl_paragraph_bold)
        style_paragraph_bold.SetNextStyle(Strings.style_paragraph)
        self._stylesheet.AddParagraphStyle(style_paragraph_bold)

        self.rtc.SetStyleSheet(self._stylesheet)
        self._style_control.SetRichTextCtrl(self.rtc)
        self._style_control.SetStyleSheet(self._stylesheet)
        self._style_control.UpdateStyles()

    def _insert_sample_text(self) -> None:
        """
        Insert sample text.
        :return: None
        """
        self.rtc.BeginParagraphStyle(Strings.style_paragraph)
        self.rtc.WriteText('Example paragraph')
        self.rtc.EndParagraphStyle()
        self.rtc.Newline()

        self.rtc.BeginParagraphStyle(Strings.style_paragraph_bold)
        self.rtc.WriteText('Example paragraph')
        self.rtc.EndParagraphStyle()
        self.rtc.Newline()

        self.rtc.BeginParagraphStyle(Strings.style_paragraph)
        self.rtc.WriteText('Example paragraph')
        self.rtc.EndParagraphStyle()
        self.rtc.Newline()


class Strings:
    """
    Just constants
    """
    style_paragraph: str = 'paragraph'
    style_paragraph_bold: str = 'boldparagraph'


class Numbers:
    paragraph_font_size: int = 12
    paragraph_font_size_1: int = 16
    paragraph_spacing: int = 20


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()

By looking into the wxWidgets c++ source code I discovered that RICHTEXT_SETSTYLE_PARAGRAPHS_ONLY has something to do with this.
The c++ code uses this:

wxRichTextParagraph* para = GetFocusObject()->GetParagraphAtPosition(GetCaretPosition()+1);
  if (para)
    return SetStyleEx(para->GetRange().FromInternal(), attr, wxRICHTEXT_SETSTYLE_WITH_UNDO|wxRICHTEXT_SETSTYLE_OPTIMIZE|wxRICHTEXT_SETSTYLE_PARAGRAPHS_ONLY);
}

By replicating this in the python code like this:

    def _style_button_handler(self, evt: wx.CommandEvent) -> None:
        """
        Handles button clicks.
        :param evt: Not used
        :return: None
        """
        style = self._stylesheet.FindParagraphStyle(Strings.style_paragraph_bold).GetStyle()
        p: rt.RichTextParagraph = self.rtc.GetFocusObject().GetParagraphAtPosition(self.rtc.GetCaretPosition()+1)
        self.rtc.SetStyleEx(p.GetRange().FromInternal(), style, flags=rt.RICHTEXT_SETSTYLE_RESET | rt.RICHTEXT_SETSTYLE_OPTIMIZE | rt.RICHTEXT_SETSTYLE_WITH_UNDO | rt.RICHTEXT_SETSTYLE_PARAGRAPHS_ONLY)

I noticed that if the rt.RICHTEXT_SETSTYLE_PARAGRAPHS_ONLY is used, it behaves the wrong way as described above, the style is applied partially onto the paragraph.
If it is not there, it changes the style correctly.

I do not know whether this is a bug or I am doing something wrong in my original code.

Is there a way how to influence which of these flags are used when changing the style from the RichTextStyleListBox?

Furthermore this is I believe the function that is called when you change the style from the RichTextStyleListBox

/// Apply a named style to the selection
bool wxRichTextCtrl::ApplyStyle(wxRichTextStyleDefinition* def) 
{
    // Flags are defined within each definition, so only certain
    // attributes are applied.
    wxRichTextAttr attr(GetStyleSheet() ? def->GetStyleMergedWithBase(GetStyleSheet()) : def->GetStyle());

    int flags = wxRICHTEXT_SETSTYLE_WITH_UNDO|wxRICHTEXT_SETSTYLE_OPTIMIZE|wxRICHTEXT_SETSTYLE_RESET;

    if (wxDynamicCast(def, wxRichTextListStyleDefinition))
    {    
        flags |= wxRICHTEXT_SETSTYLE_PARAGRAPHS_ONLY;

        wxRichTextRange range;

        if (HasSelection())
            range = GetSelectionRange();
        else
        {
            long pos = GetAdjustedCaretPosition(GetCaretPosition());
            range = wxRichTextRange(pos, pos+1);
        }

        return SetListStyle(range, (wxRichTextListStyleDefinition*) def, flags);
    }    

    bool isPara = false;

    // Make sure the attr has the style name
    if (wxDynamicCast(def, wxRichTextParagraphStyleDefinition))
    {    
        isPara = true;
        attr.SetParagraphStyleName(def->GetName());

        // If applying a paragraph style, we only want the paragraph nodes to adopt these
        // attributes, and not the leaf nodes. This will allow the content (e.g. text)
        // to change its style independently.
        flags |= wxRICHTEXT_SETSTYLE_PARAGRAPHS_ONLY;
    }
    else if (wxDynamicCast(def, wxRichTextCharacterStyleDefinition))
        attr.SetCharacterStyleName(def->GetName());
    else if (wxDynamicCast(def, wxRichTextBoxStyleDefinition))
        attr.GetTextBoxAttr().SetBoxStyleName(def->GetName());

    if (wxDynamicCast(def, wxRichTextBoxStyleDefinition))
    {
        if (GetFocusObject() && (GetFocusObject() != & GetBuffer()))
        {
            SetStyle(GetFocusObject(), attr);
            return true;
        }
        else
            return false;
    }
    else if (HasSelection())
        return SetStyleEx(GetSelectionRange(), attr, flags);
    else
    {
        wxRichTextAttr current = GetDefaultStyleEx();
        wxRichTextAttr defaultStyle(attr);
        if (isPara)
        {
            // Don't apply extra character styles since they are already implied
            // in the paragraph style
            defaultStyle.SetFlags(defaultStyle.GetFlags() & ~wxTEXT_ATTR_CHARACTER);
        }
        current.Apply(defaultStyle);
        SetAndShowDefaultStyle(current);

        // If it's a paragraph style, we want to apply the style to the
        // current paragraph even if we didn't select any text.
        if (isPara)
        {
            long pos = GetAdjustedCaretPosition(GetCaretPosition());
            wxRichTextParagraph* para = GetFocusObject()->GetParagraphAtPosition(pos);
            if (para)
            {
                return SetStyleEx(para->GetRange().FromInternal(), attr, flags);
            }
        }
        return true;
    }
}

From what I am able to understand form this without being able to run the code step by step, when you are changing the style of a paragraph as I want to do, this is used:

        // If applying a paragraph style, we only want the paragraph nodes to adopt these
        // attributes, and not the leaf nodes. This will allow the content (e.g. text)
        // to change its style independently.
        flags |= wxRICHTEXT_SETSTYLE_PARAGRAPHS_ONLY;

And this is used:

        // Don't apply extra character styles since they are already implied
        // in the paragraph style
        defaultStyle.SetFlags(defaultStyle.GetFlags() & ~wxTEXT_ATTR_CHARACTER);

And then in my case this is used:

       return SetStyleEx(para->GetRange().FromInternal(), attr, flags);

I do not understand how am I supposed to tell the code that I want the characters to change style with the paragraph. The paragraph style definition I have sets the style for the characters (weight and font size) but the second comment and code prevents this from being applied to the text. So even if I define what the text is supposed to look like in the paragraph style definition, it will have no effect and that is kind of what we see in the example.

Can someone please explain to me how this is supposed to be done?
Thank you.