Search code examples
javaswingjtextpanehtmleditorkit

JTextPane with custom document and editor does not display icons correctly


We are implementing a very small messenger like application in Java and are using a JTextPane for our messages panel. Now, we are trying to replace common smileys with .gif files and we use some example that does the job... except that the icons are center aligned and it's annoying. Is there anyway to make it look inline (left align, or normal)? Some help to resolve this would be appreciated.

Here's the initialisation code :

messagePane = new JTextPane();
messagePane.setEditable(false);
messagePane.setEditorKit(new WrapHTMLEditorKit());
messagePane.setContentType("text/html");
MessageDocument doc = new MessageDocument();
doc.addDocumentListener(new SmileyDocumentListener(messagePane));
messagePane.setStyledDocument(doc);

The MessageDocument class :

public class MessageDocument extends HTMLDocument {

    static private String NEW_LINE = System.getProperty("line.separator");

    static private SimpleAttributeSet DEFAULT_STYLE = new SimpleAttributeSet();
    static private SimpleAttributeSet ANNOUNCE_GLOBAL_STYLE = new SimpleAttributeSet();
    static private SimpleAttributeSet MESSAGE_GLOBAL_STYLE = new SimpleAttributeSet();
    static private SimpleAttributeSet MESSAGE_PRIVATE_STYLE = new SimpleAttributeSet();
    static private SimpleAttributeSet ERROR_STYLE = new SimpleAttributeSet();

    static{
        StyleConstants.setFontFamily(DEFAULT_STYLE, "Courier New");
        StyleConstants.setItalic(DEFAULT_STYLE, true);
        StyleConstants.setForeground(DEFAULT_STYLE, Color.black);       

        StyleConstants.setFontFamily(ANNOUNCE_GLOBAL_STYLE, "Courier New");
        StyleConstants.setBold(ANNOUNCE_GLOBAL_STYLE, true);
        StyleConstants.setForeground(ANNOUNCE_GLOBAL_STYLE, new Color(0, 100, 0));      

        StyleConstants.setFontFamily(MESSAGE_GLOBAL_STYLE, "Courier New");
        StyleConstants.setForeground(MESSAGE_GLOBAL_STYLE, Color.blue);

        StyleConstants.setFontFamily(MESSAGE_PRIVATE_STYLE, "Courier New");
        StyleConstants.setForeground(MESSAGE_PRIVATE_STYLE, Color.black);
        StyleConstants.setBackground(MESSAGE_PRIVATE_STYLE, new Color(220, 220, 255));

        StyleConstants.setFontFamily(ERROR_STYLE, "Courier New");
        StyleConstants.setForeground(ERROR_STYLE, Color.yellow);
        StyleConstants.setBackground(ERROR_STYLE, Color.red);

    }


    public MessageDocument() {
        super();
    }

    public void addMessage(ServerMessage msg) {
        SimpleAttributeSet style;

        if (MessageType.ANNOUNCE_GLOBAL.equals(msg.getType()) || MessageType.CONNECTION_EVENT.equals(msg.getType()) || MessageType.DISCONNECTION_EVENT.equals(msg.getType())) {
            style = ANNOUNCE_GLOBAL_STYLE;
        } else if (MessageType.MESSAGE_GLOBAL.equals(msg.getType())) {
            style = MESSAGE_GLOBAL_STYLE;
        } else if (MessageType.MESSAGE_PRIVATE.equals(msg.getType())) {
            style = MESSAGE_PRIVATE_STYLE;
        } else if (MessageType.ERROR.equals(msg.getType())) {
            style = ERROR_STYLE;
        } else {
            style = DEFAULT_STYLE;
        }

        try {
            insertString(getLength(), msg.getMessage() + NEW_LINE, style);          
        } catch (BadLocationException e) {
            e.printStackTrace();
        }
    }
}

The WrapHTMLEditorKit class :

public class WrapHTMLEditorKit extends HTMLEditorKit {

    @Override
    public ViewFactory getViewFactory() {

        return new HTMLFactory() {
            public View create(Element e) {
                View v = super.create(e);
                if (v instanceof InlineView) {
                    return new InlineView(e) {
                        public int getBreakWeight(int axis, float pos, float len) {
                            return GoodBreakWeight;
                        }

                        public View breakView(int axis, int p0, float pos, float len) {
                            if (axis == View.X_AXIS) {
                                checkPainter();
                                int p1 = getGlyphPainter().getBoundedPosition(this, p0, pos, len);
                                if (p0 == getStartOffset() && p1 == getEndOffset()) {
                                    return this;
                                }
                                return createFragment(p0, p1);
                            }
                            return this;
                        }
                    };
                } else if (v instanceof ParagraphView) {
                    return new ParagraphView(e) {
                        protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) {
                            if (r == null) {
                                r = new SizeRequirements();
                            }
                            float pref = layoutPool.getPreferredSpan(axis);
                            float min = layoutPool.getMinimumSpan(axis);
                            // Don't include insets, Box.getXXXSpan will include them.
                            r.minimum = (int) min;
                            r.preferred = Math.max(r.minimum, (int) pref);
                            r.maximum = Integer.MAX_VALUE;
                            r.alignment = 0.5f;
                            return r;
                        }

                    };
                }
                return v;
            }
        };
    }
}

And finally, the SmileyDocumentListener class :

public class SmileyDocumentListener implements DocumentListener {

    private JTextComponent owner;

    private HashMap<String,ImageIcon> smileMap;

    public SmileyDocumentListener(JTextComponent owner) {
        this.owner = owner;
        this.smileMap = new HashMap<String, ImageIcon>();

        this.smileMap.put(":)", new ImageIcon("resources/images/smileys/smile.gif"));
    }

    @Override
    public void insertUpdate(DocumentEvent event) {
        final DocumentEvent e = event;
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                if (e.getDocument() instanceof StyledDocument) {
                    try {
                        StyledDocument doc=(StyledDocument)e.getDocument();
                        int start = Utilities.getRowStart(owner, Math.max(0, e.getOffset() - 1));
                        int end = Utilities.getWordStart(owner, e.getOffset() + e.getLength());
                        String text = doc.getText(start, end-start);

                        for (String token : smileMap.keySet()) {
                            int i = text.indexOf(token);
                            while (i >= 0) {
                                final SimpleAttributeSet attrs = new SimpleAttributeSet(doc.getCharacterElement(start + i).getAttributes());
                                if (StyleConstants.getIcon(attrs) == null) {
                                    StyleConstants.setIcon(attrs, smileMap.get(token));
                                    doc.remove(start + i, 2);
                                    doc.insertString(start + i, token, attrs);
                                }
                                i = text.indexOf(token, i+token.length());
                            }
                        }
                    } catch (BadLocationException e1) {
                        e1.printStackTrace();
                    }
                }
            }
        });
    }

    @Override
    public void removeUpdate(DocumentEvent e) {}

    @Override
    public void changedUpdate(DocumentEvent e) {}

}

Note : that I've tried changing r.alignment = 0.5f; to 0.0f in WrapHTMLEditorKit.java


Solution

  • Alright, so I don't know why the icons are shown that way in a JTextPane using a content type of text/html (and I tried using a JLabel displaying an icon, setting HTML directly, etc.) Bottom line is that I thought HTML was necessary.

    I replaced

    public class MessageDocument extends HTMLDocument {
    

    by

    public class MessageDocument extends StyledDocument {
    

    I removed WrapHTMLEditorKit and modified the init code with

    messagePane = new JTextPane();
    messagePane.setEditable(false);
    messagePane.setEditorKit(new StyledEditorKit());
    MessageDocument doc = new MessageDocument();
    doc.addDocumentListener(new SmileyDocumentListener(messagePane));
    messagePane.setStyledDocument(doc);
    

    and icons displayed normally.

    The contents wraps without a special class and I can still use SimpleAttributeSet objects.