Articles Projects Tips Downloads Contacts About
 

Hanging (Negative) first line indent in JEditorPane.

There are two types of paragraph's first line indent: normal and hanging. Normal is easy achieved by specifying some positive value in appropriate attribute. For the hanging indent we have to specify not only some negative value but additionally equal positive left indent. When we do this in ParagraphView of StyledEditorKit we see some problems with select all and painting the chars of first line in newly inserted paragraph.

That could be fixed by changing ParagraphView behavior. In fact we should change child allocation rectangle obtaining in paint() method to process the negative first line indent and modelToView() method to provide correct shape for selection rendering.

The screenshot shows how it looks like:

The example code is below:

import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;
 
public class HangingIndent extends JEditorPane {

    public static void main(String[] args) {
        JFrame frame = new JFrame("Negative (Hanging) first line indent");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        final HangingIndent app = new HangingIndent();
        app.setEditorKit(new MyEditorKit());
        app.setText("The paragraph with long text is necessary to show how " +
                "hanging indent can be achieved. We should set not only the " +
                "first line indent but the same left indent.");
        StyledDocument doc=(StyledDocument)app.getDocument();
        SimpleAttributeSet attrs=new SimpleAttributeSet();
        StyleConstants.setFirstLineIndent(attrs, -50);
        StyleConstants.setLeftIndent(attrs, 50);
 
        doc.setParagraphAttributes(0,doc.getLength(),attrs, false);
 
        JScrollPane scroll = new JScrollPane(app);
        frame.getContentPane().add(scroll);
 
        frame.setSize(400, 200);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
 
    public HangingIndent() {
        super();
    }
}
 
class MyEditorKit extends StyledEditorKit {

    StyledViewFactory f=new StyledViewFactory();
    public ViewFactory getViewFactory() {
        return f;
    }
 
    class StyledViewFactory implements ViewFactory {
        public View create(Element elem) {
            String kind = elem.getName();
            if (kind != null) {
                if (kind.equals(AbstractDocument.ContentElementName)) {
                    return new LabelView(elem);
                }
                else if (kind.equals(AbstractDocument.ParagraphElementName)) {
                    return new MyParagraphView(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 MyParagraphView extends ParagraphView {
        Rectangle tempRect=new Rectangle();

 
        public MyParagraphView(Element elem) {
            super(elem);
        }
 
        public Shape modelToView(int p0, Position.Bias b0, int p1, Position.Bias b1, Shape a) 
	     	throws BadLocationException {

            Shape sh=super.modelToView(p0, b0, p1, b1,a);
            if (p0==getStartOffset() && firstLineIndent<0) {
                Rectangle r=sh instanceof Rectangle ? (Rectangle)sh :sh.getBounds();
                r.x+=firstLineIndent;
                r.width-=firstLineIndent;
                sh=r;
            }

            return sh;
        }
 
        public void paint(Graphics g, Shape a) {
            Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds();
            super.paint(g, a);
 
            // line with the negative firstLineIndent value needs
            // special handling
            if (firstLineIndent < 0) {
                Shape sh = getChildAllocation(0, a);
                if (sh!=null) {
                    Rectangle rr=sh instanceof Rectangle ? (Rectangle)sh :sh.getBounds();
                    rr.x+=firstLineIndent;
                    rr.width-=firstLineIndent;
                    if (rr.intersects(alloc)) {
                        int x = alloc.x + getLeftInset() + firstLineIndent;
                        int y = alloc.y + getTopInset();
 
                        Rectangle clip = g.getClipBounds();
                        tempRect.x = x + getOffset(X_AXIS, 0);
                        tempRect.y = y + getOffset(Y_AXIS, 0);
                        tempRect.width = getSpan(X_AXIS, 0) - firstLineIndent;
                        tempRect.height = getSpan(Y_AXIS, 0);
                        if (tempRect.intersects(clip)) {
                            tempRect.x = tempRect.x - firstLineIndent;
                            paintChild(g, tempRect, 0);
                        }
                    }
                }

            }
        }
    }
}