Articles Projects Tips Downloads Contacts About

Bullets and Numberings in the JEditorPane/JTextPane.
By Stanislav Lapitsky

The article describes how to add Bullets and Numberings (ordered and unordered lists) in the JEditorPane/JTextPane basing on StyledEditorKit. Actually HTMLEditorKit has own implementation of the feature to support <UL> and <OL> tags. This example is a bit easier because it support only one level list without nested lists but can be easy extended.

There are two types of lists - bulleted and numbered. For the numbered list start number is supported. That means user can start numbering from e.g. 100 and continue as 101, 102 etc. All the different types of numberings like Romans and alphabetical numbers aren't supported.

The screenshots below illustrate how that looks like:

The solution is based on StyledEditorKit extension. A custom document (ListDocument) was created to support ListElement which is model for the bulleted and numbered list. The element contains 2 fields for list type (bullets or numberings) and list numbering start (by default 1 and its not applied for bulleted lists).

The ListDocument has 2 methods to create and remove lists.

    protected void clearLists(int start, int end) {
        start=getParagraphElement(start).getStartOffset();
        end=getParagraphElement(end).getEndOffset();
        if (end>getLength()) {
            end=getLength();
        }
        try {
            Element root=getDefaultRootElement();
            for (int i=0; i<root.getElementCount(); i++) {
                Element elem=root.getElement(i);
                if (ListEditorKit.LIST_ELEMENT.equals(elem.getName()) &&
                        start>=elem.getStartOffset() &&
                        start<=elem.getEndOffset()) {
                    ArrayList<ElementSpec> specs=new ArrayList<ElementSpec>();
                    ElementSpec spec;
                    for (int j=0; j<elem.getElementCount(); j++) {
                        Element par=elem.getElement(j);
                        spec=new ElementSpec(par.getAttributes(), ElementSpec.StartTagType);
                        specs.add(spec);
                        for (int k=0; k<par.getElementCount(); k++) {
                            Element leaf=par.getElement(k);
                            String text=getText(leaf.getStartOffset(), leaf.getEndOffset()-leaf.getStartOffset());
                            spec=new ElementSpec(leaf.getAttributes(),
                                     ElementSpec.ContentType, text.toCharArray(), 0, text.length());
                            specs.add(spec);
                        }

                        spec=new ElementSpec(par.getAttributes(), ElementSpec.EndTagType);
                        specs.add(spec);
                    }
 
                    int elStart=elem.getStartOffset();
                    remove(elStart, elem.getEndOffset()-elStart);
 
                    if (elStart==0) {
                        insertString(0,"\n", new SimpleAttributeSet());
                    }
                    ElementSpec[] specArray = new ElementSpec[specs.size()+2];
                    ElementSpec closePar = new ElementSpec(new SimpleAttributeSet(), ElementSpec.EndTagType);
                    specArray[0]=closePar;
 
                    for (int j=0; j<specs.size(); j++) {
                        specArray[j+1]=specs.get(j);
                    }
 
                    ElementSpec openPar = new ElementSpec(new SimpleAttributeSet(), ElementSpec.StartTagType);
                    specArray[specArray.length-1]=openPar;
                    insert(Math.max(elStart, 1), specArray);
                    if (elStart==0) {
                        remove(0,1);
                    }
                }
            }
        } catch (BadLocationException e) {
            e.printStackTrace();
        }
    }
 
    protected void makeList(int start, int end, int type) {
        try {
            start=getParagraphElement(start).getStartOffset();
            end=getParagraphElement(end).getEndOffset();
            if (end>getLength()) {
                end=getLength();
            }

            clearLists(start, end-1);
 
            ArrayList<ElementSpec> specs=new ArrayList<ElementSpec>();
            ElementSpec spec;
            int offs=start;
            while (offs<end) {
                Element par=getParagraphElement(offs);
 
                spec=new ElementSpec(par.getAttributes(), ElementSpec.StartTagType);
                specs.add(spec);
                for (int i=0; i<par.getElementCount(); i++) {
                    Element leaf=par.getElement(i);
                    String text=getText(leaf.getStartOffset(), leaf.getEndOffset()-leaf.getStartOffset());
                    spec=new ElementSpec(leaf.getAttributes(),
                             ElementSpec.ContentType, text.toCharArray(), 0, text.length());
                    specs.add(spec);
                }
                spec=new ElementSpec(par.getAttributes(), ElementSpec.EndTagType);
                specs.add(spec);
 
                offs=par.getEndOffset();
            }
 
            remove(start, end-start);
            ElementSpec[] specArray = new ElementSpec[specs.size()+4];
            ElementSpec closePar = new ElementSpec(new SimpleAttributeSet(), ElementSpec.EndTagType);
            specArray[0]=closePar;
 
            SimpleAttributeSet attrs=new SimpleAttributeSet();
            attrs.addAttribute(ElementNameAttribute, ListEditorKit.LIST_ELEMENT);
            attrs.addAttribute(ElementNameAttribute, ListEditorKit.LIST_ELEMENT);
            attrs.addAttribute(ListDocument.TYPE_ATTRIBUTE_NAME, new Integer(type));
            ElementSpec areaStart = new ElementSpec(attrs, ElementSpec.StartTagType);
            specArray[1]=areaStart;
 
            for (int i=0; i<specs.size(); i++) {
                specArray[i+2]=specs.get(i);
            }
 
            ElementSpec areaEnd = new ElementSpec(attrs, ElementSpec.EndTagType);
            specArray[specArray.length-2]=areaEnd;
 
            ElementSpec openPar = new ElementSpec(attrs, ElementSpec.StartTagType);
            specArray[specArray.length-1]=openPar;
 
            insert(start, specArray);
        } catch (BadLocationException e) {
            e.printStackTrace();
        }

    }

For the ListElement rendering a custom view ListView was created. In the view we add some left indent for all list elements and override paint() method to draw list text before all the list children. For the numbers drawings we use a font with attributes used for the first text element in the list.

    public void paint(Graphics g, Shape alloc) {
        Rectangle a=alloc instanceof Rectangle ? (Rectangle)alloc : alloc.getBounds();
        super.paint(g, a);
        ListDocument.ListElement elem=(ListDocument.ListElement)getElement();
        ListDocument doc=(ListDocument)getDocument();
        int n = getViewCount();
        Rectangle clip = g.getClipBounds();
        View v=getView(0);
        while(v.getViewCount()>0) {
            v=v.getView(0);
        }
        Font f=doc.getFont(v.getAttributes());
        g.setFont(f);
        String s="999.";
        if (elem.type==ListDocument.TYPE_BULLET) {
            s="\u2022 ";
        }

        int w=g.getFontMetrics(f).stringWidth(s);
        int x = a.x + getLeftInset()-w;
        int y = a.y + getTopInset();
        for (int i = 0; i < n; i++) {
            y=a.y + getTopInset()+getOffset(Y_AXIS, i);
 
            v=getView(i);
            while(v.getViewCount()>0) {
                v=v.getView(0);
            }
            if (v instanceof LabelView) {
                y+=((LabelView)v).getGlyphPainter().getAscent((LabelView)v);
            }
 
            s=(i+elem.start)+".";
            if (elem.type==ListDocument.TYPE_BULLET) {
                s="\u2022";
            }

            g.drawString(s,x,y);
        }
    }

In most of editors (e.g. MS Word) list members numberings is stopped when user types 2 paragraph break chars (e.g. by pressing ENTER key twice). To achieve this StyledInsertBreakAction was customized and the kit replaces original action with the new one. All the action changes is removing 2 last paragraph elements from list when user inserts break in the last empty paragraph in the list.

    public Action[] getActions() {
        Action[] res=super.getActions();
        for (int i=0; i<res.length; i++) {
            if (insertBreakAction.equals(res[i].getValue(Action.NAME)) ) {
                res[i]=new StyledInsertBreakAction();
            }
        }
        return res;
    }
 
    static class StyledInsertBreakAction extends StyledTextAction {
        private SimpleAttributeSet tempSet;
 

        StyledInsertBreakAction() {
            super(insertBreakAction);
        }
 
        public void actionPerformed(ActionEvent e) {
            JEditorPane target = getEditor(e);
 
            if (target != null) {
                if ((!target.isEditable()) || (!target.isEnabled())) {
                    UIManager.getLookAndFeel().provideErrorFeedback(target);
                    return;
                }
                StyledEditorKit sek = getStyledEditorKit(target);
 
                if (tempSet != null) {
                    tempSet.removeAttributes(tempSet);
                }
                else {
                    tempSet = new SimpleAttributeSet();
                }
                tempSet.addAttributes(sek.getInputAttributes());
 
                ListDocument doc=(ListDocument)target.getDocument();
                int offs=target.getCaretPosition();
                ListDocument.ListElement list=doc.getListElement(offs);
                if (list!=null && offs==list.getEndOffset()-1) {
                    Element last=list.getElement(list.getElementCount()-1);
                    if (last.getEndOffset()-last.getStartOffset()==1) {
                        //empty par
                        try {
                            doc.remove(offs-1, 1);
 
                            doc.insertString(offs, "\n", last.getAttributes());
                            doc.insertString(offs, "\n", last.getAttributes());
                            target.setCaretPosition(target.getCaretPosition()+2);
                        } catch (BadLocationException e1) {
                            e1.printStackTrace();
                        }
                    }
                    else {
                        target.replaceSelection("\n");
                    }

                }
                else {
                    target.replaceSelection("\n");
                }
 
                MutableAttributeSet ia = sek.getInputAttributes();
 
                ia.removeAttributes(ia);
                ia.addAttributes(tempSet);
                tempSet.removeAttributes(tempSet);
            }
            else {
                // See if we are in a JTextComponent.
                JTextComponent text = getTextComponent(e);
 
                if (text != null) {
                    if ((!text.isEditable()) || (!text.isEnabled())) {
                        UIManager.getLookAndFeel().provideErrorFeedback(target);
                        return;
                    }
                    text.replaceSelection("\n");
                }
            }
        }
    }

If you need more information about this the bullets_numberings.jar library contains full source code and runnable example.

Back to Table of Content