Recently I have been working on a Java text editor project and I would like to use a JTextPane
to replace the old JTextArea
in order to implement syntax highlighting. However, a JTextPane
lacks methods in JTextArea
(such as append()
, getLineStartOffset()
etc) and I want to reimplement them in my class MyTextPane
(a sub-class of JTextPane
) but have run into troubles.
My current code (only a small self-contained part):
import java.awt.*;
import javax.swing.*;
import javax.swing.text.*;
public class MyTextPane extends JTextPane
{
public MyTextPane()
{
super();
}
public void append(String text)
{
try
{
Document doc = this.getDocument();
doc.insertString(doc.getLength(),text,null);
}
catch (BadLocationException ex)
{
//must succeed
throw new InternalError(ex.getMessage());
}
}
public void insert(String text, int pos)
{
try
{
this.getStyledDocument().insertString(pos,text,null);
}
catch (BadLocationException ex)
{
throw new IllegalArgumentException(ex);
}
}
public void replaceRange(String str, int start, int end)
{
try
{
Document doc = this.getDocument();
doc.remove(start,end-start);
doc.insertString(start,str,null);
}
catch (BadLocationException ex)
{
throw new IllegalArgumentException(ex);
}
}
public void setLineWrap(boolean isLineWrap)
{
/*
* implements later
*/
}
public boolean getLineWrap()
{
/*
* implements later
*/
return true;
}
public void setWrapStyleWord(boolean isWrapStyleWord)
{
/*
* implements later
*/
}
public boolean getWrapStyleWord()
{
/*
* implements later
*/
return true;
}
public void setTabSize(int size)
{
/*
* implements later
*/
}
public int getTabSize()
{
/*
* implements later
*/
return 4;
}
public int getLineCount()
{
//follow JTextArea implementation
Element root = this.getDocument().getDefaultRootElement();
return root.getElementCount();
}
public int getLineStartOffset(int line) throws BadLocationException
{
//follow JTextArea implementation
int count = this.getLineCount();
Document doc = this.getDocument();
if (line < 0)
{
throw new BadLocationException("Negative line", -1);
}
if (line >= count)
{
throw new BadLocationException("No such line", doc.getLength() + 1);
}
return doc.getDefaultRootElement().getElement(line).getStartOffset();
}
public int getLineEndOffset(int line) throws BadLocationException
{
//follow JTextArea implementation
int count = this.getLineCount();
Document doc = this.getDocument();
if (line < 0)
{
throw new BadLocationException("Negative line", -1);
}
if (line >= count)
{
throw new BadLocationException("No such line", doc.getLength() + 1);
}
int end = doc.getDefaultRootElement().getElement(line).getEndOffset();
return (line==count-1)?(end-1):end;
}
public int getLineOfOffset(int off) throws BadLocationException
{
//follow JTextArea implementation
Document doc = this.getDocument();
if (off < 0)
{
throw new BadLocationException("Can't translate offset to line", -1);
}
if (off > doc.getLength())
{
throw new BadLocationException("Can't translate offset to line", doc.getLength() + 1);
}
return doc.getDefaultRootElement().getElementIndex(off);
}
public static void main(String[] args)
{
final SimpleAttributeSet BOLD_SET = new SimpleAttributeSet();
StyleConstants.setBold(BOLD_SET, true);
StyleConstants.setForeground(BOLD_SET, new Color(0,0,125));
SwingUtilities.invokeLater(new Runnable()
{
@Override
public void run()
{
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
MyTextPane textPane = new MyTextPane();
frame.add(new JScrollPane(textPane), BorderLayout.CENTER);
frame.setSize(200,200);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
}
As you can see, I have already added back some methods like append()
. However, I can't think of any ways to control the line wrap policy.
The default behavior is quite strange: when there are one short word and one long word,
if I consecutively enter characters without a space,
it firstly appears like wrapping by words. However when I continue to enter characters,
it does not wrap at all.
Is there any elegant way to control the wrapping policy of a JTextPane
? In other words, can a JTextPane
wrap words like a JTextArea
? I found so many duplicates (like this, this and this) but couldn't find a solution. Thanks in advance.
This is a discussion about the same problem: Word wrapping behavior in JTextPane since Java 7. The solution proposed by user StanislavL (who also appears to be very active on Stack Overflow: StanislavL) to support word wrapping works for me using Java 8. It uses a custom WrapEditorKit
as the editor kit for the JTextPane
(and the WrapEditorKit
class in turn uses the WrapColumnFactory
and WrapLabelView
classes).
Combining this with the NonWrappingTextPane example (from the book Core Swing: Advanced Programming by Kim Topley) makes it possible to switch line wrapping off:
import java.awt.*;
import javax.swing.*;
public class WrapTestApp extends JFrame {
public static void main(final String[] arguments) {
new WrapTestApp();
}
public WrapTestApp() {
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setBounds(800, 400, 200, 200);
getContentPane().setLayout(new BorderLayout());
final CustomTextPane textPane = new CustomTextPane(true);
final JScrollPane scrollPane = new JScrollPane(textPane);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
getContentPane().add(scrollPane, BorderLayout.CENTER);
textPane.setText("ExampleOfTheWrapLongWordWithoutSpaces");
setVisible(true);
}
}
The CustomTextPane
class:
import javax.swing.*;
import javax.swing.text.*;
public class CustomTextPane extends JTextPane {
private boolean lineWrap;
public CustomTextPane(final boolean lineWrap) {
this.lineWrap = lineWrap;
if (lineWrap)
setEditorKit(new WrapEditorKit());
}
@Override
public boolean getScrollableTracksViewportWidth() {
if (lineWrap)
return super.getScrollableTracksViewportWidth();
else
return getParent() == null
|| getUI().getPreferredSize(this).width <= getParent().getSize().width;
}
private class WrapEditorKit extends StyledEditorKit {
private final ViewFactory defaultFactory = new WrapColumnFactory();
@Override
public ViewFactory getViewFactory() {
return defaultFactory;
}
}
private class WrapColumnFactory implements ViewFactory {
@Override
public View create(final Element element) {
final String kind = element.getName();
if (kind != null) {
switch (kind) {
case AbstractDocument.ContentElementName:
return new WrapLabelView(element);
case AbstractDocument.ParagraphElementName:
return new ParagraphView(element);
case AbstractDocument.SectionElementName:
return new BoxView(element, View.Y_AXIS);
case StyleConstants.ComponentElementName:
return new ComponentView(element);
case StyleConstants.IconElementName:
return new IconView(element);
}
}
// Default to text display.
return new LabelView(element);
}
}
private class WrapLabelView extends LabelView {
public WrapLabelView(final Element element) {
super(element);
}
@Override
public float getMinimumSpan(final int axis) {
switch (axis) {
case View.X_AXIS:
return 0;
case View.Y_AXIS:
return super.getMinimumSpan(axis);
default:
throw new IllegalArgumentException("Invalid axis: " + axis);
}
}
}
}