I've got a JTextArea with setLineWrap(true)
and (for now) setWrapStyleWord(true)
.
The text which is contained in that textArea contains almost no white space, and so the wrapping never occurs. But the text is semi-colon separated. And so I'd like to achieve a wrap style at the ";" instead of at the " ".
With the following text:
hello;world;foo;bar;I am the Wizard;of;Oz
Wrapping like
hello;world;foo;bar;
I am the Wizard;
of;Oz
Instead of (with setWrapStyleWord(true)
)
hello;world;foo;bar;I
am the Wizard;of;Oz
or
Instead of (with setWrapStyleWord(false)
)
hello;world;foo;bar;I a
m the Wizard;of;Oz
Any idea on how to realize this ?
CodePudding user response:
The solution is to tweak the ComponentUI used by the JTextArea. See this discussion.
One could create directly a new ComponentUI and assign it to the JTextArea, but I recommend to create a new JTextArea subclass handling all this under the radar.
The Component
package com.zparkingb.swing;
import com.zparkingb.swing.ui.SeparatedTextAreaUI;
import javax.swing.JTextArea;
import javax.swing.UIManager;
import javax.swing.text.Document;
public class ZSeparatedTextArea extends JTextArea {
private final Character wordSeparator;
private static final String uiClassID = "SeparatedTextAreaUI";
public ZSeparatedTextArea(Character separator) {
super();
this.wordSeparator = separator;
}
public ZSeparatedTextArea(Character separator, String text) {
super(text);
this.wordSeparator = separator;
}
public ZSeparatedTextArea(Character separator, int rows, int columns) {
super(rows, columns);
this.wordSeparator = separator;
}
public ZSeparatedTextArea(Character separator, String text, int rows, int columns) {
super(text, rows, columns);
this.wordSeparator = separator;
}
public ZSeparatedTextArea(Character separator, Document doc) {
super(doc);
this.wordSeparator = separator;
}
public ZSeparatedTextArea(Character separator, Document doc, String text, int rows, int columns) {
super(doc, text, rows, columns);
this.wordSeparator = separator;
}
public Character getWordSeparator() {
return wordSeparator;
}
public void setUI(SeparatedTextAreaUI ui) {
super.setUI(ui);
}
@Override
public void updateUI() {
if (UIManager.get(getUIClassID()) != null) {
SeparatedTextAreaUI ui = (SeparatedTextAreaUI) UIManager.getUI(this);
setUI(ui);
}
else {
setUI(new SeparatedTextAreaUI());
}
}
public SeparatedTextAreaUI getUI() {
return (SeparatedTextAreaUI) ui;
}
@Override
public String getUIClassID() {
return uiClassID;
}
}
The ComponentUI
package com.zparkingb.swing.ui;
import com.zparkingb.swing.ZSeparatedTextArea;
import javax.swing.JComponent;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicTextAreaUI;
import javax.swing.text.Element;
import javax.swing.text.View;
public class SeparatedTextAreaUI extends BasicTextAreaUI {
private ZSeparatedTextArea textArea = null;
/**
* Creates the view for an element. Returns a SeparatedWrappedPlainView (or WrappedPlainView/PlainView if no wordSeparator is provided)
*
* @param elem the element
*
* @return the view
*/
public View create(Element elem) {
if (textArea.getWordSeparator() == null)
return super.create(elem);
View v = new SeparatedWrappedPlainView(textArea.getWordSeparator(), elem);
return v;
}
public static ComponentUI createUI(JComponent c) {
return new SeparatedTextAreaUI();
}
@Override
public void installUI(JComponent c) {
textArea = (ZSeparatedTextArea) c;
super.installUI(c);
}
@Override
public void uninstallUI(JComponent c) {
super.uninstallUI(c);
textArea = null;
}
}
The View
This one is more tricky. As some information required to do the word wrapping are private in SeparatedWrappedPlainView
they have been duplicated (e.g. metrics
)
package com.zparkingb.swing.ui;
import java.awt.Container;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Shape;
import java.text.BreakIterator;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.Segment;
import javax.swing.text.TabExpander;
import javax.swing.text.Utilities;
import javax.swing.text.WrappedPlainView;
class SeparatedWrappedPlainView extends WrappedPlainView {
private int _tabBase;
private FontMetrics _metrics;
private final Character wordSeparator;
public SeparatedWrappedPlainView(Character wordSeparator, Element elem) {
super(elem);
assert(wordSeparator!=null);
this.wordSeparator=wordSeparator;
}
/**
* Rem: Copied from WrappedPlainView only to be able to use our own
* getBreakLocation instead of the {@link Utilities#getBreakLocation} used by default
*/
@Override
protected int calculateBreakPosition(int p0, int p1) {
Segment s = new Segment();
try {
getDocument().getText(p0, p1 - p0, s);
} catch (BadLocationException ex) {
assert false : "Couldn't load text";
}
int width = getWidth();
int pos;
pos = p0 getBreakLocation(s, _metrics, _tabBase, _tabBase width, this, p0);
return pos;
}
/**
* Rem: Copied from {@link Utilities#getBreakLocation} in order to
* to break the text on our separator instead of on whitespaces.
*/
private int getBreakLocation(Segment s, FontMetrics metrics, float x0, float x, TabExpander e, int startOffset) {
char[] txt = s.array;
int txtOffset = s.offset;
int txtCount = s.count;
int index = Utilities.getTabbedTextOffset(s, metrics, x0, x, e, startOffset, false); //, null, useFPIAPI);
if (index >= txtCount - 1) {
return txtCount;
}
for (int i = txtOffset index; i >= txtOffset; i--) {
char ch = txt[i];
if (ch < 256) {
// break on separator
if (wordSeparator.equals(ch)) {
index = i - txtOffset 1;
break;
}
}
else {
// a multibyte char found; use BreakIterator to find line break
BreakIterator bit = BreakIterator.getLineInstance();
bit.setText(s);
int breakPos = bit.preceding(i 1);
if (breakPos > txtOffset) {
index = breakPos - txtOffset;
}
break;
}
}
return index;
}
void _updateMetrics() {
Container component = getContainer();
_metrics = component.getFontMetrics(component.getFont());
}
@Override
public void paint(Graphics g, Shape a) {
Rectangle r = a instanceof Rectangle ? (Rectangle) a : a.getBounds();
_tabBase = r.x;
_updateMetrics();
super.paint(g, a);
}
@Override
public float getPreferredSpan(int axis) {
_updateMetrics();
return super.getPreferredSpan(axis);
}
@Override
public float getMaximumSpan(int axis) {
_updateMetrics();
return super.getMaximumSpan(axis);
}
@Override
public float getMinimumSpan(int axis) {
_updateMetrics();
return super.getMinimumSpan(axis);
}
@Override
public void setSize(float width, float height) {
_updateMetrics();
super.setSize(width, height);
}
}