I'm making a simple markdown text editor with Swing which can render markdown formatting live in its main JTextPane. That is, markdown-formatted text like *hello*
is displayed in italics once detected, however is saved in plain text.
To this end I came up with a regex to pick up markdown tokens (let's just use *italics*
as an example for now), and every few seconds my code searches the text of the JTextPane and uses JTextPane#setCharacterAttributes to change the formatting of areas in question.
// init
PLAIN = Document.addStyle("plain", null);
StyleConstants.setFontSize(PLAIN, 12);
ITALIC = Document.addStyle("italic", null);
StyleConstants.setItalic(ITALIC, true);
...
// every few seconds
// remove all formatting
Document.setCharacterAttributes(0, Document.getLength(), PLAIN, true);
// italicize parts that the regex matches
m = Pattern.compile("\\*([^\\n*]+)\\*").matcher(temp);
while (m.find()) {
Document.setCharacterAttributes(m.start(), m.group().length(), ITALIC, false);
}
The problem is the 'liveness' - after some time, the JTextPane starts wrapping by characters instead of words, and at times loses word wrapping altogether and simply displays unwrapped lines.
Is there any way I can fix this/extend the JTextPane to fix it, or is the JTextPane simply not suited to such live updating? I googled for a really long time but couldn't find anything; I'm just not sure what to search for.
package test;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.lang.reflect.Field;
import java.util.logging.*;
import java.util.regex.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
public class Test {
private JFrame frame = new JFrame();
private JTextPane jtp;
private StyledDocument doc;
// NEW LINES
private Timer T;
private boolean update = true;
MarkdownRenderer m;
// END OF NEW LINES
public Test() {
jtp = new JTextPane();
jtp.setEditorKit(new MyStyledEditorKit());
jtp.setText("\ntype some text in the above empty line and check the wrapping behavior");
doc = jtp.getStyledDocument();
// NEW LINES
m = new MarkdownRenderer(jtp);
Timer T = new Timer(2000, new java.awt.event.ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (update) {
update = false;
m.render();
}
}
});
T.start();
// END OF NEW LINES
doc.addDocumentListener(new DocumentListener() {
private boolean doUpdate = true;
public void insertUpdate(DocumentEvent e) {
insert();
}
public void removeUpdate(DocumentEvent e) {
insert();
}
public void changedUpdate(DocumentEvent e) {
// triggers every time formatting is changed
// insert();
}
public void insert() {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
Style defaultStyle = jtp.getStyle(StyleContext.DEFAULT_STYLE);
doc.setCharacterAttributes(0, doc.getLength(), defaultStyle, false);
update = true;
}
});
}
});
JScrollPane scroll = new JScrollPane(jtp);
scroll.setPreferredSize(new Dimension(200, 200));
frame.add(scroll);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
Test bugWrapJava7 = new Test();
}
});
}
}
class MyStyledEditorKit extends StyledEditorKit {
private MyFactory factory;
public ViewFactory getViewFactory() {
if (factory == null) {
factory = new MyFactory();
}
return factory;
}
}
class MyFactory implements ViewFactory {
public View create(Element elem) {
String kind = elem.getName();
if (kind != null) {
if (kind.equals(AbstractDocument.ContentElementName)) {
return new MyLabelView(elem);
} else if (kind.equals(AbstractDocument.ParagraphElementName)) {
return new ParagraphView(elem);
} else if (kind.equals(AbstractDocument.SectionElementName)) {
return new BoxView(elem, View.Y_AXIS);
} else if (kind.equals(StyleConstants.ComponentElementName)) {
return new ComponentView(elem);
} else if (kind.equals(StyleConstants.IconElementName)) {
return new IconView(elem);
}
}
// default to text display
return new LabelView(elem);
}
}
class MyLabelView extends LabelView {
public MyLabelView(Element elem) {
super(elem);
}
public View breakView(int axis, int p0, float pos, float len) {
if (axis == View.X_AXIS) {
resetBreakSpots();
}
return super.breakView(axis, p0, pos, len);
}
private void resetBreakSpots() {
try {
// HACK the breakSpots private fields
Field f=GlyphView.class.getDeclaredField("breakSpots");
f.setAccessible(true);
f.set(this, null);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MarkdownRenderer {
private static final MutableAttributeSet PLAIN = new SimpleAttributeSet();
private static final MutableAttributeSet BOLD = new SimpleAttributeSet();
private static final MutableAttributeSet ITALIC = new SimpleAttributeSet();
private static final MutableAttributeSet UNDERLINE = new SimpleAttributeSet();
private StyledDocument Document = null;
public MarkdownRenderer(JTextPane editor) {
Document = (StyledDocument) editor.getDocument();
StyleConstants.setBold(BOLD, true);
StyleConstants.setItalic(ITALIC, true);
StyleConstants.setUnderline(UNDERLINE, true);
}
void render() {
String s = "";
try {
s = Document.getText(0, Document.getLength());
} catch (BadLocationException ex) {
Logger.getLogger(MarkdownRenderer.class.getName()).log(Level.SEVERE, null, ex);
}
// Document.setCharacterAttributes(0, Document.getLength(), PLAIN, true);
String temp = s.replaceAll("\\*\\*([^\\n*]+)\\*\\*", "|`$1`|"); // can also use lazy quantifier: (.+?)
Matcher m = Pattern.compile("\\|`.+?`\\|").matcher(temp);
while (m.find()) {
Document.setCharacterAttributes(m.start(), m.group().length(), BOLD, false);
}
m = Pattern.compile("\\*([^\\n*]+)\\*").matcher(temp);
while (m.find()) {
Document.setCharacterAttributes(m.start(), m.group().length(), ITALIC, false);
}
m = Pattern.compile("_+([^\\n*]+)_+").matcher(temp);
while (m.find()) {
Document.setCharacterAttributes(m.start(), m.group().length(), UNDERLINE, false);
}
}
}