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 - Нет комментариев, тоже самое.
оплаченный
Для тех, кто нуждается в более конкретном объяснении особого способа обработки списков 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);
}
}