JTextPane - Bullet with HTMLEditorKit list not rendering correctly unless I do setText(getText()) and repaint

StackOverflow https://stackoverflow.com/questions/17522022

Pregunta

I have:

JTextPane jtextPane = new JTextPane();
jtextPane.setEditorKit(new HTMLEditorKit());
...

Then later I try to add an unordered list button to the toolbar such that the action is:

Action insertBulletAction = 
        HTMLEditorKit.InsertHTMLTextAction ("Bullets", "<ul><li> </li></ul>", 
                                            HTML.Tag.P, HTML.Tag.UL);
JButton insertBulletJButton = new JButton(insertBulletAction);

And this does include the proper code if I take a dump of the html that's generated. However it will be rendered really badly, as in not even close to reasonable as illustrated below:

snapshot of generated bullet

However if I do:

jtextPane.setText(jtextPane.getText());
jtextPane.repaint();

Then all is well. But if I don't do BOTH lines, then neither will work alone. I can also make it work by setting the text before I make the jtextPane visible.

This is really odd and I don't understand why I'd have to do a setText(getText()) followed by a repaint().

PS: This is very similar to this question: How to implement bullet points in a JTextPane? And it works except that it's not rendering correctly. I don't know if it has to do with HTMLEditorKit vs RTFEditorKit, but something i causing the rendering to fail. The html source code under is perfect...

PS2: This link is also very handy but it also didn't show a solution.

Update: Here is the full code as requested, but there isn't much else...

public static void main(String[] args)
{
    JFrame jframe = new JFrame();
    jframe.setSize(800, 600);
    jframe.setVisible(true);

    JTextPane jtextPane = new JTextPane();
    jtextPane.setEditorKit(new HTMLEditorKit());

    Action insertBulletAction = new HTMLEditorKit.InsertHTMLTextAction ("Bullets", 
                                    "<ul><li> </li></ul>", HTML.Tag.P, HTML.Tag.UL);
    JButton insertBulletJButton = new JButton(insertBulletAction);
    insertBulletJButton.setRequestFocusEnabled(false);

    jframe.setLayout(new BorderLayout());
    jframe.add(new JScrollPane(jtextPane));
    jframe.add(insertBulletJButton, BorderLayout.SOUTH);
}
¿Fue útil?

Solución

The answer is actually quite complex. Basically the InsertHtmlAction is just not good enough by itself. You need a lot of work and logic to get to a working list action. It requires a lot of logic! So you definitely have to override the Action class. Basically the parameters of the InsertHtmlAction will change depending on where in the html code you are.

That being said, I studied several open source solutions to get a better understanding for what was all involved. Many long hours later (and many hours spent beforehand as well), and I was finally able to figure it out well enough for what I needed. But it is fairly complex. Too complex to write about here, it would take a chapter of a book just to explain the concepts. And even then I'm still fuzzy on some details (I'm still working through it).

I can now understand why people sell components for this!

I found that most open source solutions don't really deal nicely with lists. They generally somewhat work but most have glaring bugs. That or they just don't really handle anything but the most basic cases of lists.

Here is a list of systems I looked at to understand how they work to get a better understanding of everything. I unfortunately found the documentation lacking or hard to understand, so looking at these projects helped me more than anything else.

The most helpful

  • Shef - Most helpful of all.
  • ekit - Decent but many bugs and not the best code organization
  • MetaphaseEditor - Similar to ekit

Moderately helpful (more complex, buggy, less relevant, etc.)

  • OOoBean - Tried but too much (and hence too much complexity) for what I needed. Looks really good though, you just need to invest time.
  • JXHTMLEdit - Seemed interest

Additional links

  • JWebEngine - Mostly for rendering
  • Joeffice - Interesting but it's all videos and wasn't enough ready enough yet.
  • Richtext - No comments. I only briefly looked at it.
  • JRichTextEditor - No comments either, same thing.

Paid

  • JWord - Look very interesting but it was beyond the budget for what I was doing.

Otros consejos

For those that need a more specific explanation of HTMLEditorKit's peculiar way of handling lists, it all comes down to the markup generated. I'll try it to keep it as simple as I can. Let's rewind a bit and talk about HTML documents in Swing.

Turns out that Swing relies on paragraphs for doing cursor positioning and navigation. For example, every time you write in a new line, a new pagraph is generated. Even the corresponding view of the document depends on the presence of paragraphs in the right places. There must always be a paragraph in the document. Otherwise, odd things start to happen.

So, what happens if the document is completely blank? Surely, there is no need for a paragraph there. Well, unbelievably, even in that case there is a paragraph. This is one of the effects of what the documentation calls a p-implied or implied paragraph. The HTML generated for a blank document is:

<html>
  <head></head>
  <body>
    <p style="margin-top: 0">

    </p>
  </body>
</html>

Expectedly, when you insert a list, it is placed inside the paragraph:

<html>
  <head></head>
  <body>
    <p style="margin-top: 0">
      <ul>
        <li>

        </li>
      </ul>
    </p>
  </body>
</html>

... which is of course invalid markup (not just because there is no title inside the head). But wait! It gets more interesting. Once the list is inserted, the "internal pointer" of the document, as it were, stays after the closing </ul> tag. Consequently, if you type "Hello", it will be placed outside the list:

<html>
  <head></head>
  <body>
    <p style="margin-top: 0">
      <ul>
        <li>

        </li>
      </ul>
      Hello
    </p>
  </body>
</html>

This is why that “Hello” appears way to the right relative to the inserted bullet. Now, as Stephane mentioned in the question, setText(getText()) magically solves the problem. That's because manually setting the contents of the JTextPane instance triggers the parser, which in turn places the “internal pointer” where it should be; inside the list. Now when you type “Hello”, it will appear much closer to the bullet. I say much closer because there is still something not right about the HTML:

<html>
  <head></head>
  <body>
    <p style="margin-top: 0">
      <ul>
        <li>
          Hello
        </li>
      </ul>      
    </p>
  </body>
</html>

Notice there is no paragraph enclosing the new text in the list. That's why the text won't appear right next to the bullet.

How do you go about all this? Well, that's the tricky bit Stephane was talking about. You would be up against a combination of bugs (such as this one), undocumented glitches (like this one) and default behaviour as we have seen. The easiest way out is to use one of the solutions in Stephane's list. I agree Shef is the best of all but has not have that much activity since 2009 (!). Personally, I found Stanislav's website incredibly useful for all things EditorKit.

You can also have a look at ADAPRO: a pretty stable open-source assistive editor I was heavily involved in. The assistive features are buggy but the core editing functionality was thoroughly tested. The following code comes from that project. It requires the ElementWriter class from SHEF's net.atlanticbb.tantlinger.ui.text package.

    //HTML representation of an empty paragraph
    private static final String sEmptyParagraph = "<p style=\"margin-top: 0\"></p>";

    /**
     * Translates into HTML a given element of the document model.
     * @param element Element to serialise to a HTML string
     * @param out Serialiser to HTML string
     * @return HTML string "equivalent" to given element
     */
    static String extractHTML (Element element, StringWriter out) {

        ElementWriter writer = new ElementWriter (out, element);
        try {
            writer.write();
        } catch (IOException e) {
                System.out.println ("Error encountered when serialising element: " +e);
                e.printStackTrace();
        } catch (BadLocationException e) {
                System.out.println ("Error encountered when extracting HTML at the element's position: " +e); 
                e.printStackTrace();
        }
        return out.toString();
    }

    /**
     * Determines if the parent element of the current paragraph element is one of a number provided as a list
     * of tag names. If so, it returns the parent element.
     * @param document Document model of the text
     * @param iCaretPos Caret's current position
     * @param sTags Possible parent tags
     * @return Parent element
     */
    static Element getNearestParent (HTMLDocument document, int iCaretPos, String sTags) {
        Element root;

        root = document.getParagraphElement (iCaretPos);
        do {
           root = root.getParentElement();
        } while (sTags.indexOf (root.getName()) ==  -1);
        return root;
    }

    /**
     * Inserts all HTML tags required to build an ordered/unordered list at the caret's current position. 
     * If the aim is instead to turn the numbered/bulleted paragraphs into plain ones, it takes care of 
     * deleting the necessary tags.
     * @param sTypeList Type of list to build: "ul" or "ol". 
     * @param textArea Editable area containing text.   
     */
    static void insertList (String sTypeList, JTextPane textArea) {
        boolean bOnlyListSelected;          //selection includes a list exclusively                 
        int iStartIndex, iEndIndex,         //element indexes included in selection 
            iStartSel, iEndSel,             //starting and ending offset of selected text
            iItemNo,                        //total number of list items
            i;
        String sHTML,                       //HTML code of text represented by a given element
               sHTMLBlock,                  //HTML code block to be inserted into document model
               sRest;                       //part of the text remaining unselected after the selected block                
        HTML.Tag tag;                       //current list tag
        HTMLDocument document;              //data model underlying the typed text
        Element root,                       //root element of the document model tree
                section;                    //element representing a block of text              
        SimpleAttributeSet attribIns;       //backup of current input attributes            

        //Fetches the current document
        document = (HTMLDocument) textArea.getDocument();

        //Finds the topmost parent element of the current paragraph (effectively, is the list inside a table?)
        root = getNearestParent (document, textArea.getCaretPosition(), "td body");

        //Range of elements included in the selection
        iStartSel = textArea.getSelectionStart();
        iEndSel = textArea.getSelectionEnd();
        iStartIndex = root.getElementIndex (iStartSel);
        iEndIndex = root.getElementIndex (iEndSel);

        //HTML-related initialisations
        sHTML = "";
        sHTMLBlock = "";
        tag = null;

        //Checks if selection is comprised of just list items
        i = iStartIndex;
        bOnlyListSelected = true;
        do {
           tag = HTML.getTag (root.getElement(i).getName());

           //Is it a list tag?
           if ((tag == null) || ((!tag.equals (HTML.Tag.OL)) && (!tag.equals (HTML.Tag.UL))))
              bOnlyListSelected = false;
           i++;
        } while (bOnlyListSelected && (i <= iEndIndex)); 

        //Back up current input attributes
        attribIns = new SimpleAttributeSet (textArea.getInputAttributes());

        try {
            //At some point in the selection there is no previous list... 
            if (!bOnlyListSelected) {

               //Inserts <LI> tags for every text block
               for (i = iStartIndex; i <= iEndIndex; i++) {
                   section = root.getElement(i);
                   tag = HTML.getTag (section.getName());

                   //Retrieves current HTML
                   sHTML = extractHTML (section, new StringWriter());

                   //If it is non-listed text, reconstitute the paragraph
                   if (tag == null)
                      sHTML = "<p style=\"margin-top: 0;\">" +sHTML+ "</p>";

                   //Text in a list already => no nesting (delete <UL>/<OL> tags)
                   if (sHTML.indexOf("<li>") != -1) { 
                      sHTML = sHTML.substring (sHTML.indexOf("<li>"), sHTML.length());
                      sHTML = sHTML.substring (0, sHTML.lastIndexOf("</li>") + 5);

                   //Non-listed text => add <LI> tags     
                   } else sHTML = "<li>" +sHTML+ "</li>"; 

                   sHTMLBlock = sHTMLBlock + sHTML;                  
               }
               sHTMLBlock = "<"+sTypeList+">" +sHTMLBlock.trim()+ "</"+sTypeList+">";

               //Gets the text coming after caret or end of selection
               sRest = textArea.getText (iEndSel, document.getLength() - iEndSel);

               //Adds an empty paragraph at the end of the list if the latter coincides with the end of the document
               //or if the rest of the document is empty. This is to avoid a glitch in the editor kit's write() method.
               //http://java-sl.com/tip_html_kit_last_empty_par.html               
               if ((root.getElement(iEndIndex).getEndOffset() == root.getEndOffset()) ||
                   sRest.replaceAll ("[\\p{Z}\\s]", "").trim().isEmpty())
                  sHTMLBlock = sHTMLBlock + sEmptyParagraph;

               //Removes the remaining old non-listed text block and saves resulting HTML string to document model
               document.setOuterHTML (root.getElement(iEndIndex), sHTMLBlock);
               if (iEndIndex > iStartIndex)
                  document.remove (root.getElement(iStartIndex).getStartOffset(), 
                                   root.getElement(iEndIndex - 1).getEndOffset() - 
                                   root.getElement(iStartIndex).getStartOffset());

            //Selection just includes list items
            } else {

                   //Works out the list's length in terms of element indexes
                   root = root.getElement (root.getElementIndex (iStartSel));
                   iItemNo = root.getElementCount();
                   iStartIndex = root.getElementIndex (textArea.getSelectionStart());
                   iEndIndex = root.getElementIndex (textArea.getSelectionEnd());

                   //For everery <LI> block, remove the <LI> tag
                   for (i = iStartIndex; i <= iEndIndex; i++) {
                       sHTML = extractHTML (root.getElement(i), new StringWriter());        
                       sHTML = sHTML.substring(sHTML.indexOf("<li>") + 4, sHTML.length());
                       sHTML = sHTML.substring(0, sHTML.lastIndexOf("</li>"));
                       sHTMLBlock = sHTMLBlock + sHTML;                      
                   }

                   //List selected partially? => divide list
                   if (iItemNo > (iEndIndex - iStartIndex + 1)) {

                      //Saves HTML string to document model
                      ((HTMLEditorKit) textArea.getEditorKit()).insertHTML (document, root.getElement(iEndIndex).getEndOffset(), 
                                            sHTMLBlock, 3, 0, HTML.Tag.P);

                      //Removes the old block 
                      document.remove (root.getElement(iStartIndex).getStartOffset(), 
                                       root.getElement(iEndIndex).getEndOffset() - 
                                       root.getElement(iStartIndex).getStartOffset());

                   //Removes the list tag associated with the block    
                   } else document.setOuterHTML (root, sHTMLBlock.trim());                     
            }

        } catch (Exception eTexto) {
                System.out.println ("Problemas al eliminar/insertar texto: " +eTexto);
                eTexto.printStackTrace();
        }

        //Recover selection. Previous operations displace the cursor and thus selection highlight is lost
        textArea.setSelectionStart (iStartSel);
        textArea.setSelectionEnd (iEndSel);

        //If only one list item has been created and is the first one, copy all previous style information to the list
        if ((!bOnlyListSelected) && (iStartSel == iEndSel)) {
           textArea.setCharacterAttributes (attribIns, false); 
        }        
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top