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:
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:
So basicially it thinks the image is less high, but draws it fully anyway.
This mostly works, however there are two issues with that:
- The additional height still only gets added on the top, it might be nicer if it's evenly distributed.
- But more importantly sometimes the bottom part isn't drawn, for example when scrolling up and then down again:
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.";
}
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:
Instead of adding the Icon to the document maybe add a
Box.createHorizontalStrut(...)
to reserve space for the width of theIcon
.Then you can use the
createPosition()
method of the document to get position of where theIcon
should be added.You create a
HashMap
to store thePosition
andIcon
objects.Then you override the
paintComponent()
method of the text pane to iterate through theHashMap
and paint all the Icons. ThePosition
object will contain the offset where the component was added. You can use themodelToView(...)
method of theDocument
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.