JTextPane - Пуля со списком HTMLEditorKit не отображается правильно, если я не сделаю setText(getText()) и перекрасить

Я имею:

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

Затем позже я пытаюсь добавить кнопку неупорядоченного списка на панель инструментов так, чтобы действие было:

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

И это включает в себя правильный код, если я возьму дамп HTML, который генерируется. Тем не менее, он будет отображаться очень плохо, так как даже близко к разумному, как показано ниже:

снимок сгенерированной пули

Однако если я сделаю:

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

Тогда все хорошо. Но если я не делаю ОБА строки, то ни один не будет работать в одиночку. Я также могу заставить это работать, устанавливая текст прежде, чем я сделаю jtextPane видимым.

Это действительно странно, и я не понимаю, почему я должен сделать setText(getText()) с последующим repaint(),

PS: Это очень похоже на этот вопрос: как реализовать маркеры в JTextPane? И это работает за исключением того, что он не рендерится правильно. Я не знаю, имеет ли это отношение к HTMLEditorKit против RTFEditorKit, но что-то я вызывал сбой рендеринга. HTML-код под идеальным...

PS2: Эта ссылка также очень удобна, но она также не показывает решения.

Обновление: здесь приведен полный код в соответствии с запросом, но больше ничего нет...

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);
}

2 ответа

Решение

Ответ на самом деле довольно сложный. В основном InsertHtmlAction просто не достаточно хорош сам по себе. Вам нужно много работы и логики, чтобы перейти к действию рабочего списка. Это требует много логики! Так что вам обязательно нужно переопределить класс Action. В основном параметры InsertHtmlAction будет меняться в зависимости от того, где в HTML-коде вы находитесь.

Тем не менее, я изучил несколько решений с открытым исходным кодом, чтобы лучше понять, что было вовлечено. Много долгих часов спустя (а также много часов, проведенных заранее), и я наконец смог понять это достаточно хорошо для того, что мне было нужно. Но это довольно сложно. Здесь слишком сложно писать, для объяснения понятий понадобилась бы глава книги. И даже тогда я все еще размыта по некоторым деталям (я все еще работаю над этим).

Теперь я могу понять, почему люди продают компоненты для этого!

Я обнаружил, что большинство решений с открытым исходным кодом не очень хорошо справляются со списками. Обычно они работают, но у большинства есть явные ошибки. Это или они просто не обрабатывают ничего, кроме самых основных случаев списков.

Вот список систем, на которые я посмотрел, чтобы понять, как они работают, чтобы лучше понять все. К сожалению, я обнаружил, что документация отсутствует или трудна для понимания, поэтому просмотр этих проектов помог мне больше всего на свете.

Самый полезный

  • Шеф - самый полезный из всех.
  • ekit - Достойный, но много ошибок и не самая лучшая организация кода
  • MetaphaseEditor - похож на ekit

Умеренно полезный (более сложный, ошибочный, менее актуальный и т. Д.)

  • OOoBean - пробовал, но слишком много (и, следовательно, слишком много) для того, что мне нужно. Выглядит очень хорошо, хотя, вам просто нужно потратить время.
  • JXHTMLEdit - Кажущийся интерес

Дополнительные ссылки

  • JWebEngine - в основном для рендеринга
  • Joeffice - Интересно, но это все видео и еще недостаточно готово.
  • Richtext - Без комментариев. Я только кратко посмотрел на это.
  • JRichTextEditor - Нет комментариев, тоже самое.

оплаченный

  • JWord - Выглядит очень интересно, но это было за рамки бюджета на то, что я делал.

Для тех, кто нуждается в более конкретном объяснении особого способа обработки списков HTMLEditorKit, все сводится к сгенерированной разметке. Я постараюсь сделать это как можно проще. Давайте немного перемотаем и поговорим о HTML-документах в Swing.

Оказывается, что Swing использует абзацы для позиционирования курсора и навигации. Например, каждый раз, когда вы пишете в новой строке, генерируется новый абзац. Даже соответствующий вид документа зависит от наличия абзацев в нужных местах. В документе всегда должен быть параграф. В противном случае странные вещи начинают происходить.

Итак, что произойдет, если документ будет полностью пустым? Конечно, там нет необходимости в абзаце. Ну, невероятно, даже в этом случае есть параграф. Это один из эффектов того, что в документации называется p-подразумеваемым или подразумеваемым абзацем. HTML-код, сгенерированный для пустого документа:

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

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

Как ожидается, когда вы вставляете список, он помещается внутри абзаца:

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

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

... что, конечно, неверная разметка (не только потому, что внутри заголовка нет заголовка). Но ждать! Это становится более интересным. После того, как список вставлен, "внутренний указатель" документа как бы остается после закрытия </ul> тег. Следовательно, если вы введете "Hello", он будет размещен вне списка:

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

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

Вот почему это "Привет" появляется справа от вставленной пули. Теперь, как Стефан упомянул в вопросе, setText(getText()) волшебным образом решает проблему. Это происходит потому, что ручная установка содержимого экземпляра JTextPane запускает синтаксический анализатор, который, в свою очередь, размещает "внутренний указатель" там, где он должен быть; внутри списка. Теперь, когда вы наберете "Hello", оно появится намного ближе к пуле. Я говорю намного ближе, потому что в HTML все еще есть что-то не так:

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

Обратите внимание, что в списке нет абзаца, включающего новый текст. Вот почему текст не появится рядом с маркером.

Как вы идете обо всем этом? Ну, это хитрый вопрос, о котором говорил Стефан. Вы бы столкнулись с комбинацией ошибок (таких как эта), недокументированных сбоев (как эта) и поведения по умолчанию, как мы видели. Самый простой выход - использовать одно из решений в списке Стефана. Я согласен, что Шеф - лучший из всех, но с 2009 года он не проявлял особой активности (!). Лично я нашел сайт Станислава невероятно полезным для всех вещей EditorKit.

Вы также можете взглянуть на ADAPRO: довольно стабильный вспомогательный редактор с открытым исходным кодом, в котором я активно участвовал. Вспомогательные функции содержат ошибки, но основные функции редактирования были тщательно протестированы. Следующий код взят из этого проекта. Для этого требуется класс ElementWriter из пакета SHEF net.atlanticbb.tantlinger.ui.text.

    //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); 
        }        
}
Другие вопросы по тегам