Add image to JTextPane that overlaps lines to save space

188 views Asked by At

I have a JTextPane with a StyledDocument and need to add an image between text that is higher than the font. This means that normally the line gets higher:

enter image description here

What I'm aiming for is to have the image slightly overlap, so it uses less space. In the MCVE I achieve this by returning a smaller vertical span for the IconView, which looks like this:

enter image description here

So basicially it thinks the image is less high, but draws it fully anyway.

This mostly works, however there are two issues with that:

  1. The additional height still only gets added on the top, it might be nicer if it's evenly distributed.
  2. But more importantly sometimes the bottom part isn't drawn, for example when scrolling up and then down again:

enter image description here

Quite understandable, given that the lower part isn't really where the image is expected to be.

My question is now, is there a better (less hacky) way to prevent the image from taking up so much space? Or at least a way to fix the drawing? I already dug around the code a bit, but I'm not sure what part is responsible for deciding what is drawn or how to best change it without messing anything else up.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.BoxView;
import javax.swing.text.ComponentView;
import javax.swing.text.Element;
import javax.swing.text.IconView;
import javax.swing.text.LabelView;
import javax.swing.text.ParagraphView;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.StyledEditorKit;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;

public class OverlappingImage {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            createGui();
        });
    }

    private static void createGui() {
        JTextPane textPane = new JTextPane();
        textPane.setEditorKit(new MyEditorKit());
        textPane.setEditable(false);
        textPane.setPreferredSize(new Dimension(320, 200));

        StyledDocument doc = textPane.getStyledDocument();
        SimpleAttributeSet iconStyle = new SimpleAttributeSet();
        StyleConstants.setIcon(iconStyle, createImage());

        try {
            doc.insertString(doc.getLength(), TEST_TEXT+"\n"+TEST_TEXT, null);
            doc.insertString(doc.getLength(), "Image", iconStyle);
            doc.insertString(doc.getLength(), TEST_TEXT, null);
        } catch (BadLocationException ex) {
            Logger.getLogger(OverlappingImage.class.getName()).log(Level.SEVERE, null, ex);
        }

        JFrame window = new JFrame();
        window.add(new JScrollPane(textPane), BorderLayout.CENTER);
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.pack();
        window.setLocationByPlatform(true);
        window.setTitle("Test");
        window.setVisible(true);
    }

    static class MyEditorKit extends StyledEditorKit {

        private final ViewFactory factory;

        public MyEditorKit() {
            this.factory = new StyledViewFactory();
        }

        @Override
        public ViewFactory getViewFactory() {
            return factory;
        }

        static class StyledViewFactory implements ViewFactory {

            @Override
            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 ParagraphView(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 MyIconView(elem);
                    }
                }
                return new LabelView(elem);
            }

        }
    }

    static class MyIconView extends IconView {

        public MyIconView(Element elem) {
            super(elem);
        }

        @Override
        public float getPreferredSpan(int axis) {
            if (axis == View.Y_AXIS) {
                float height = super.getPreferredSpan(axis);
                return height * 0.7f;
            }
            return super.getPreferredSpan(axis);
        }
    }

    /**
     * Creates the example image.
     */
    public static ImageIcon createImage() {
        BufferedImage image = new BufferedImage(28,28, BufferedImage.TYPE_INT_ARGB);
        Graphics g = image.getGraphics();
        g.setColor(Color.GREEN);
        g.fillRect(0, 0, 28, 28);
        g.setColor(Color.BLACK);
        g.drawRect(0, 0, 27, 27);
        g.dispose();
        return new ImageIcon(image);
    }

    private static final String TEST_TEXT = "Lorem ipsum dolor sit amet, "
            + "consectetur adipisici elit, sed eiusmod tempor incidunt ut "
            + "labore et dolore magna aliqua. Ut enim ad minim veniam, quis "
            + "nostrud exercitation ullamco laboris nisi ut aliquid ex ea "
            + "commodi consequat. Quis aute iure reprehenderit in voluptate "
            + "velit esse cillum dolore eu fugiat nulla pariatur. Excepteur "
            + "sint obcaecat cupiditat non proident, sunt in culpa qui officia "
            + "deserunt mollit anim id est laborum.";

}
1

There are 1 answers

0
camickr On

is there a better (less hacky) way to prevent the image from taking up so much space? Or at least a way to fix the drawing?

Don't know if this is better or less hacky (or if it will work), so I'll let you decide.

Maybe you can do your own custom painting of the image:

  1. Instead of adding the Icon to the document maybe add a Box.createHorizontalStrut(...) to reserve space for the width of the Icon.

  2. Then you can use the createPosition() method of the document to get position of where the Icon should be added.

  3. You create a HashMap to store the Position and Icon objects.

  4. Then you override the paintComponent() method of the text pane to iterate through the HashMap and paint all the Icons. The Position object will contain the offset where the component was added. You can use the modelToView(...) method of the Document to get the x/y location of the component in the text pane. Do your math to vertically center the Icon and then paint it.