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

烂漫一生 提交于 2019-12-01 14:06:23

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.

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); 
        }        
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!