Home > Enterprise >  Custom JTextArea wrap word style
Custom JTextArea wrap word style

Time:12-27

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