javaswingword-wrapjtextarea

Custom JTextArea wrap word style


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 ?


Solution

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