Cross Platform Rich Text Editor

Just starting out? Need help? Post your questions and find answers here.
russellm72
User
User
Posts: 17
Joined: Wed Jul 13, 2022 2:04 pm

Cross Platform Rich Text Editor

Post by russellm72 »

Hello!

I'm a new user, investigating transitioning from Livecode. It seems that there isn't an easy cross platform way to use rich text / styled text in the EditorGadget? Is this true? If so, what's the best way to use rich / styled text in PB?

Note: I mainly want to be able to use strikethrough styling on individual lines of text.

Thanks in advance.
Axolotl
Addict
Addict
Posts: 802
Joined: Wed Dec 31, 2008 3:36 pm

Re: Cross Platform Rich Text Editor

Post by Axolotl »

I can think of three extension solutions from memory.

1. idle's lightweight editorgadgetEx
2. PBEdit - a Canvas-based Texteditor by Mr.L
3. EditorEx by Thorsten1867

In addition there are also markdown editor implementations.
Not to forget the scintilla dll used in Pb
Maybe there is more.
Just because it worked doesn't mean it works.
PureBasic 6.04 (x86) and <latest stable version and current alpha/beta> (x64) on Windows 11 Home. Now started with Linux (VM: Ubuntu 22.04).
User avatar
Shardik
Addict
Addict
Posts: 2058
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Re: Cross Platform Rich Text Editor

Post by Shardik »

russellm72 wrote: Tue Jul 19, 2022 3:14 pm It seems that there isn't an easy cross platform way to use rich text / styled text in the EditorGadget? Is this true? If so, what's the best way to use rich / styled text in PB?
Indeed there is no easy cross-platform way. But already 5 years ago I have posted this cross-platform code example in the German forum which allows to use text tags to convert marked text into bold, italic and underlined text. In MacOS and Windows the text is transformed into the RTF text format. Since Linux doesn't support the RTF format, the marked text is converted using GTK tags. Therefore you currently have to exchange marked text between the different operating systems before displaying it in the EditorGadget because the text in Linux - as mentioned - doesn't use the RTF format. But my special format before displaying it in the EditorGadget is cross-platform.

I have modified the source code to also support strikethrough text by extending the tag definitions in the data section. I have successfully tested the modified source code on these operating systems:
  • Linux Mint 19.3 'Tricia' x64 with Cinnamon and both GTK2 and GTK3 using PB 6.00 x64
  • MacOS 10.13.6 'High Sierra' with PB 6.00 x64
  • MacOS 11.6.7 'Big Sur' with PB 6.00 x64 in Light and Dark Mode
  • Raspbian GNU/Linux 11 (Bullseye) ARM32 with PB 6.00 ARM32 (C backend)
  • Windows 10 x64 21H2 with PB 6.00 x86 and x64
These are screenshots from running the code example:

Linux Mint 19.3 'Tricia' with Cinnamon and GTK3
Image

MacOS 10.13.6 'High Sierra'
Image

Raspbian GNU/Linux 11 (Bullseye)
Image

Windows 10 21H2
Image

Code: Select all

EnableExplicit

CompilerIf #PB_Compiler_OS = #PB_OS_Linux
  ImportC ""
     gtk_text_buffer_create_tag(*Buffer.GtkTextBuffer, TagCharacter.P-UTF8,
       PropertyName.P-UTF8, PropertyValue.I, Terminator.I = 0)
  EndImport
CompilerEndIf

Structure TagEntry
  Character.S
  AttributeName.S
  StartOffset.I
  EndOffset.I
EndStructure

Procedure AddTextWithAttributes(EditorGadgetID.I, AttributeText.S)
  Static NewList Tag.TagEntry()

  Protected Offset.I
  Protected RTFTag.S
  Protected TagCharacter.S
  Protected TagList.S
  Protected TagOffset.I

  CompilerSelect #PB_Compiler_OS
    CompilerCase #PB_OS_Linux
      Static TagTable.I

      Protected AttributeValue.S
      Protected EndIter.GtkTextIter
      Protected NewTextOffset.I
      Protected StartIter.GtkTextIter
      Protected *TextBuffer = gtk_text_view_get_buffer_(0 +
        GadgetID(EditorGadgetID))
    CompilerCase #PB_OS_MacOS
      Protected AttributeString.I
      Protected AttributeTextASCII.S
      Protected DataObject.I
      Protected TextStorage.I
    CompilerCase #PB_OS_Windows
      Protected AttributeTextASCII.S
  CompilerEndSelect

  ; ----- On first call create new tag table and define RTF tags (for MacOS and
  ;       and Windows) or the Linux-specific tags
 
  If ListSize(Tag()) = 0
    Repeat
      Read.S TagCharacter
     
      If TagCharacter = ""
        Break
      Else
        AddElement(Tag())
        Tag()\Character = TagCharacter
        Read.S Tag()\AttributeName

        CompilerIf #PB_Compiler_OS = #PB_OS_Linux
          Read.S AttributeValue
          gtk_text_buffer_create_tag(*TextBuffer, Tag()\Character,
            Tag()\AttributeName, Val(AttributeValue))
        CompilerEndIf
      EndIf
    ForEver
  Else
    ForEach Tag()
      Tag()\StartOffset = 0
      Tag()\EndOffset = 0
    Next
  EndIf

  ; ----- Find all tags and fill in tag character, attribute name, start-
  ;       and stop offset in LinkedList Tag()       

  Repeat
    Offset = FindString(AttributeText, "[")
   
    If Offset = 0
      Break
    Else
      TagList = ""
      TagOffset = Offset + 1
      RTFTag = "{"
     
      Repeat
        TagCharacter = Mid(AttributeText, TagOffset, 1)
       
        If TagCharacter = "]"
          Break
        Else
          If FindString(TagList, TagCharacter) = 0
            TagList + TagCharacter
          EndIf
         
          ForEach Tag()
            If Tag()\Character = TagCharacter
              If Tag()\StartOffset = 0
                Tag()\StartOffset = Offset
                RTFTag + "\" + Tag()\AttributeName + " "
              Else
                Tag()\EndOffset = Offset
                RTFTag = "}"
              EndIf

              Break
            EndIf
          Next
        EndIf
       
        TagOffset + 1
      ForEver

      CompilerIf #PB_Compiler_OS = #PB_OS_Linux
        AttributeText = ReplaceString(AttributeText, "[" + TagList + "]", "",
          #PB_String_NoCase, Offset, 1)
      CompilerElse
        AttributeText = ReplaceString(AttributeText, "[" + TagList + "]",
          RTFTag, #PB_String_NoCase, Offset, 1)
        AttributeText = ReplaceString(AttributeText, #CR$, "\line")
      CompilerEndIf

      TagList = ""
    EndIf
  ForEver

  ; ----- Append new text with attributes at already existing text

  CompilerSelect #PB_Compiler_OS
    CompilerCase #PB_OS_Linux
      NewTextOffset = gtk_text_buffer_get_char_count_(*TextBuffer)
      gtk_text_buffer_get_end_iter_(*TextBuffer, @EndIter)
      gtk_text_buffer_insert_(*TextBuffer, @EndIter, AttributeText, -1)
     
      If ListSize(Tag()) > 0
        ; ----- Fill in all tags from list in *TextBuffer

        ForEach Tag()
          gtk_text_buffer_get_iter_at_offset_(*TextBuffer, @StartIter,
            NewTextOffset + Tag()\StartOffset - 1)
          gtk_text_buffer_get_iter_at_offset_(*TextBuffer, @EndIter,
            NewTextOffset + Tag()\EndOffset - 1)
          gtk_text_buffer_apply_tag_by_name_(*TextBuffer, Tag()\Character,
            @StartIter, @EndIter)
        Next
      EndIf
    CompilerCase #PB_OS_MacOS
      AttributeText = "{\rtf1" + AttributeText + "}"
      AttributeTextASCII = Space(StringByteLength(AttributeText, #PB_Ascii))
      PokeS(@AttributeTextASCII, AttributeText, -1, #PB_Ascii)
      DataObject = CocoaMessage(0, 0,
        "NSData dataWithBytes:", @AttributeTextASCII, 
        "length:", Len(AttributeText))

      If DataObject
        AttributeString = CocoaMessage(0, 0, "NSAttributedString alloc")
        CocoaMessage(@AttributeString, AttributeString,
          "initWithRTF:@", @DataObject,
          "documentAttributes:", 0)
 
        If AttributeString
          TextStorage = CocoaMessage(0, GadgetID(0), "textStorage")
          CocoaMessage(0, TextStorage, "appendAttributedString:",
            AttributeString)
          CocoaMessage(0, AttributeString, "release")
        EndIf

        ; ----- Automatically set correct text color in light and dark mode

        CocoaMessage(0, GadgetID(EditorGadgetID),
          "setTextColor:", CocoaMessage(0, 0, "NSColor textColor"))
      EndIf
    CompilerCase #PB_OS_Windows
      AttributeText = "{\rtf1" + AttributeText + "}"
      AttributeTextASCII = Space(StringByteLength(AttributeText, #PB_Ascii))
      PokeS(@AttributeTextASCII, AttributeText, -1, #PB_Ascii)
      SendMessage_(GadgetID(EditorGadgetID), #EM_SETSEL, -1, -1)
      SendMessage_(GadgetID(EditorGadgetID), #EM_REPLACESEL, 0,
        AttributeTextASCII)
  CompilerEndSelect
EndProcedure

OpenWindow(0, 100, 100, 400, 100, "EditorGadget with text attributes")
EditorGadget(0, 10, 10, WindowWidth(0) - 20, WindowHeight(0) - 20)

AddTextWithAttributes(0,
  "This is [b]bold text[b] and the following is [u]underlined[u]." + #CR$)
AddTextWithAttributes(0, "Now follows [i]italic[i] and [s]strikethrough[s]." +
  #CR$)
AddTextWithAttributes(0,
  "And combined [biu]bold, italic and underlined[biu].")

Repeat
Until WaitWindowEvent() = #PB_Event_CloseWindow

End

; ----- Tag definitions

DataSection
  CompilerIf #PB_Compiler_OS = #PB_OS_Linux
    Data.S "b", "weight", "700"
    Data.S "i", "style", "2"
    Data.S "s", "strikethrough", "1"
    Data.S "u", "underline", "1"
  CompilerElse
    Data.S "b", "b"
    Data.S "i", "i"
    Data.S "s", "strike"
    Data.S "u", "ul"
  CompilerEndIf

  Data.S ""
EndDataSection
Last edited by Shardik on Wed Jul 20, 2022 9:33 pm, edited 2 times in total.
russellm72
User
User
Posts: 17
Joined: Wed Jul 13, 2022 2:04 pm

Re: Cross Platform Rich Text Editor

Post by russellm72 »

But already 5 years ago I have posted this cross-platform code example in the German forum which allows to use text tags to convert marked text into bold, italic and underlined text.
Wow! Thank you! This looks really cool. I've been reading through your code a bit trying to get familiar with it and to understand it.

One thing I'm trying to wrap my head around is how I would change one line of text within an EditGadget.

I'm used to being able to do something like this (in Livecode):

Code: Select all

set the textStyle of line tCurrLineNum of field "FieldCommandsList" to "strikeout"
Or (again in Livecode):

Code: Select all

set textStyle of line tIndex of field "FieldCommandsList" to "plain"
If you were using your code to change a single line's styling (among a bunch of already existing lines of text that the user has entered into the field/gadget), how would you do it? (I'm not yet familiar with processing text in PB.)

Thanks in advance for any guidance or advice. And, seriously, thanks again for sharing this, it looks really promising and very impressive.
User avatar
Shardik
Addict
Addict
Posts: 2058
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Re: Cross Platform Rich Text Editor

Post by Shardik »

russellm72 wrote: Wed Jul 20, 2022 4:12 pm Wow! Thank you! This looks really cool. I've been reading through your code a bit trying to get familiar with it and to understand it.
Thank you for your praise and interest. Until now you have been the first one. Because of the missing general interest I didn't develop the code any further.

russellm72 wrote: Wed Jul 20, 2022 4:12 pm One thing I'm trying to wrap my head around is how I would change one line of text within an EditGadget

Until now you are only able to display preformatted text cross-platform with these predefined tags as defined in the DataSection:

Code: Select all

[b]Text[b] = display "Text" in bold
[i]Text[i] = display "Text" in italics
[s]Text[s] = display "Text" as strikethrough
[u]Text[u] = display "Text" as underlined

If you have the need to change the format of a text selected with the cursor dynamically, the example code has to be expanded! Please tell me whether you need this additional feature and I might try to implement it.

If you want to change the text format of an existing text in the EditorGadget by position index and string length or by searching for a text string and changing its format, this would be much easier to implement.
User avatar
idle
Always Here
Always Here
Posts: 5836
Joined: Fri Sep 21, 2007 5:52 am
Location: New Zealand

Re: Cross Platform Rich Text Editor

Post by idle »

We really need to work more at saying thankyou and showing some appreciation. Looks good Shardik I might use it.
russellm72
User
User
Posts: 17
Joined: Wed Jul 13, 2022 2:04 pm

Re: Cross Platform Rich Text Editor

Post by russellm72 »

Shardik wrote: Wed Jul 20, 2022 8:07 pm If you have the need to change the format of a text selected with the cursor dynamically, the example code has to be expanded! Please tell me whether you need this additional feature and I might try to implement it.

If you want to change the text format of an existing text in the EditorGadget by position index and string length or by searching for a text string and changing its format, this would be much easier to implement.
So, for my immediate needs, I would just need the ability to add or remove the strikethrough style from one or more lines of text. I'm guess from what you're saying that that would be by position index and string length? I haven't even investigated enough to know how to identify a line of text using PB because I've been very spoiled in Livecode with its ability to just use line numbers within any given chunk of text. I don't know, but I'm guessing that in PB you have to iterate character by character looking for line endings and keep track of their offsets in order to identify lines of text?

However, in thinking about things, I do think your code would be much more useful if it had the ability to reverse the process and take RTF/styled text from the EditGadget and convert it back to text with simple tags. Saying that, I have no idea how difficult a task that would be but it definitely would be extremely useful.

Anyway, thanks again for providing your code. It does look very promising.
User avatar
Shardik
Addict
Addict
Posts: 2058
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Re: Cross Platform Rich Text Editor

Post by Shardik »

russellm72 wrote: Thu Jul 21, 2022 10:15 am So, for my immediate needs, I would just need the ability to add or remove the strikethrough style from one or more lines of text.
Linux (tested successfully on Linux Mint 19.3 'Tricia' x64 Cinnamon and PB 6.00 x64):

Image


MacOS (tested successfully on MacOS Catalina and Big Sur with PB 6.00):

Image


Windows (tested successfully on Windows 10 x64 21H2 with PB 6.00 x86):

Image


Code: Select all

EnableExplicit

Define FontSize.I
Define i.I
Define LineCount.I
Define LineNumber.I
Define TextLine.S
Define WindowHeight.I

#Window = 0

Enumeration Gadgets
  #Editor
  #Text
  #ComboBox
EndEnumeration

Declare ToggleStrikethroughInLine(EditorID.I, LineNumber.I)

CompilerSelect #PB_Compiler_OS
  CompilerCase #PB_OS_Linux
    FontSize = 21
  CompilerCase #PB_OS_MacOS
    #NSUnderlineStyleThick = 2
    FontSize = 27
  CompilerDefault
    FontSize = 21
CompilerEndSelect

OpenWindow(#Window, 200, 100, 270, 194, "EditorGadget")
EditorGadget(#Editor, 23, 20, WindowWidth(#Window) - 45,
  WindowHeight(#Window) - 90)
SetGadgetFont(#Editor, LoadFont(0, "Arial", FontSize))
TextGadget(#Text, 10, GadgetHeight(#Editor) + 40, 180, 25,
  "Toggle strikethrough in line", #PB_Text_Right)
ComboBoxGadget(#ComboBox, GadgetWidth(#Text) + 18, GadgetHeight(#Editor) + 37,
  50, 25)

CompilerIf #PB_Compiler_OS = #PB_OS_Linux
  ; ----- Define Callback when popup list of ComboBox is closed because on
  ;       Linux the non-editable ComboBox doesn't produce a change event, if
  ;       the currently selected item is chosen again

  ProcedureC ComboBoxPopupCallback(*ComboBox.GtkComboBox,
    *ParamSpec.GParamSpec, *UserData)
    Protected IsComboBoxPopupOpen.I
    
    g_object_get_(GadgetID(#ComboBox), "popup-shown", @IsComboBoxPopupOpen)
    
    If IsComboBoxPopupOpen = #False
      PostEvent(#PB_Event_Gadget, #Window, #ComboBox, #PB_EventType_Change)
    EndIf
  EndProcedure
  
  g_signal_connect_(GadgetID(#ComboBox), "notify::popup-shown",
    @ComboBoxPopupCallback(), 0)
CompilerEndIf

Repeat
  Read.S TextLine

  If TextLine <> #EOT$
    AddGadgetItem(#Editor, -1, TextLine)
    LineCount + 1
  EndIf 
Until TextLine = #EOT$

For i = 0 To LineCount - 1
  AddGadgetItem(#ComboBox, -1, Str(i + 1))
Next i

Dim IsStrikethrough.I(LineCount - 1)
SetGadgetState(#ComboBox, 0)

Repeat
  Select WaitWindowEvent()
    Case #PB_Event_CloseWindow
      Break
    Case #PB_Event_Gadget
      If EventGadget() = #ComboBox
        If EventType() = #PB_EventType_Change
          LineNumber = GetGadgetState(#ComboBox)
          ToggleStrikethroughInLine(#Editor, LineNumber)
        EndIf
      EndIf
  EndSelect
ForEver

End

Procedure ToggleStrikethroughInLine(EditorID.I, LineNumber.I)
  Shared IsStrikethrough.I()

  Protected TextLine.S

  TextLine = GetGadgetItemText(EditorID, LineNumber)
  IsStrikethrough(LineNumber) ! 1
  
  CompilerSelect #PB_Compiler_OS
    CompilerCase #PB_OS_Linux ; -----------------------------------------------
      Static *StrikethroughTag.GtkTextTag
      
      Protected EndIter.GtkTextIter
      Protected StartIter.GtkTextIter
      Protected *TextBuffer
      
      *TextBuffer = gtk_text_view_get_buffer_(GadgetID(EditorID))
      gtk_text_buffer_get_iter_at_line_(*TextBuffer,
        @StartIter, LineNumber)
      gtk_text_buffer_get_iter_at_offset_(*TextBuffer,
        @EndIter, gtk_text_iter_get_offset_(@StartIter) + Len(TextLine))
      
      If *StrikethroughTag = 0
        *StrikethroughTag = gtk_text_buffer_create_tag_(*TextBuffer,
          "s", "strikethrough", #True)
      EndIf
      
      If IsStrikethrough(LineNumber)
        gtk_text_buffer_apply_tag_(*TextBuffer, *StrikethroughTag,
          @StartIter, @EndIter)
      Else
        gtk_text_buffer_remove_tag_(*TextBuffer, *StrikethroughTag,
          @StartIter, @EndIter)
      EndIf
    CompilerCase #PB_OS_MacOS ; -----------------------------------------------
      Protected AttributedString.I
      Protected i.I
      Protected Location.I
      Protected Range.NSRange
      Protected TextStorage.I

      TextStorage = CocoaMessage(0, GadgetID(EditorID), "textStorage")
      AttributedString = CocoaMessage(0, CocoaMessage(0, 0,
        "NSMutableAttributedString alloc"), "initWithString:$", @TextLine)
      
      For i = 0 To LineNumber - 1
        Location + Len(GetGadgetItemText(EditorID, LineNumber)) + 2
      Next i
      
      Range\location = Location
      Range\length = Len(TextLine) + 1
      
      If IsStrikethrough(LineNumber)
        CocoaMessage(0, TextStorage,
          "addAttribute:$", @"NSStrikethrough",
          "value:", CocoaMessage(0, 0,
          "NSNumber numberWithInteger:", #NSUnderlineStyleThick),
          "range:@", @Range)
        CocoaMessage(0, AttributedString, "release")
      Else
        CocoaMessage(0, TextStorage,
          "removeAttribute:$", @"NSStrikethrough",
          "range:@", @Range)
      EndIf
    CompilerCase #PB_OS_Windows ; ---------------------------------------------
      Protected Format.CHARFORMAT2 
      Protected Range.CHARRANGE

      ; ----- Select text
      Range\cpMin = SendMessage_(GadgetID(0), #EM_LINEINDEX,
        LineNumber, 0)
      Range\cpMax = Range\cpMin + Len(TextLine)
      SendMessage_(GadgetID(EditorID), #EM_EXSETSEL, 0, @Range) 

      ; ----- Set/reset strikethrough
      Format\cbSize = SizeOf(CHARFORMAT2) 
      Format\dwMask = #CFM_STRIKEOUT

      If IsStrikethrough(LineNumber)
        Format\dwEffects = #CFM_STRIKEOUT
      Else
        Format\dwEffects = 0
      EndIf

      SendMessage_(GadgetID(EditorID), #EM_SETCHARFORMAT, #SCF_SELECTION,
        @Format)

      ; ----- Unselect text
      SendMessage_(GadgetID(EditorID), #EM_HIDESELECTION, 1, 0) 
  CompilerEndSelect
EndProcedure

DataSection
  Data.S "The quick brown"
  Data.S "fox jumps over"
  Data.S "the lazy dog."
  Data.S #EOT$
EndDataSection
Post Reply