I have a JTextPane that I add text to, whereas some of the text has an image set via StyleConstants.setIcon()
. I also add a mouse listener to the JTextPane to detect when the mouse is clicked on/hovered over an image, however it only detects it on the left part of the image. Am I doing something wrong?
SSCCE (hovering over the image changes the mouse cursor to indicate when it detects the image):
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
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.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
/**
* SSCCE to show how detecting an image under the current mouse position only
* works on part of the image. It adds a simple image to the document of the
* JTextPane and changes the mouse cursor when it detects the mouse hovering
* over the image.
*/
public class JTextPaneImage {
private static void createWindow() {
// Create window
final JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Create JTextPane and add to window
final JTextPane textPane = new JTextPane();
textPane.setEditable(false);
textPane.addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
AttributeSet style = getAttributes(e);
if (style != null && StyleConstants.getIcon(style) != null) {
textPane.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
} else {
textPane.setCursor(Cursor.getDefaultCursor());
}
}
});
frame.add(new JScrollPane(textPane));
try {
StyledDocument doc = (StyledDocument)textPane.getDocument();
// Add some text
doc.insertString(0, "Some text ", null);
// Add the image
SimpleAttributeSet style = new SimpleAttributeSet();
StyleConstants.setIcon(style, createImage());
doc.insertString(doc.getLength(), "test", style);
} catch (BadLocationException ex) {
Logger.getLogger(JTextPaneImage.class.getName()).log(Level.SEVERE, null, ex);
}
// Display everything
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
/**
* Retrieves the style of where the mouse is positioned (assuming this is
* a JTextPane).
*
* @param e The mouse event containing the mouse position
* @return The AttributeSet or null if none could be found
*/
private static AttributeSet getAttributes(MouseEvent e) {
JTextPane text = (JTextPane)e.getSource();
Point mouseLocation = new Point(e.getX(), e.getY());
int pos = text.viewToModel(mouseLocation);
if (pos >= 0) {
StyledDocument doc = text.getStyledDocument();
Element element = doc.getCharacterElement(pos);
return element.getAttributes();
}
return null;
}
/**
* Creates a single 28x28 image filled with a single color.
*
* @return The created ImageIcon
*/
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.dispose();
return new ImageIcon(image);
}
public static final void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createWindow();
}
});
}
}
You use strong text.viewToModel(mouseLocation)
to detect offset and then retrieve the style from obtained offset.
The logic is to return closer offset to the mouse position. Thus when you click on the right half of view the next offset is returned (offset after the view). You can try the same setting a big letter (e.g. m with big font). When you clikc close to the letter end caret is set to be after the letter. Here the logic is the same.
So you got position after the image and get style from the position but after the image view text element don't have the icon in the attributes and you gor no image.
UPDATE:
To provide correct behaviour I would suggest to use modelToView() and pass obtained offset. From the rectangle you can figure out whether your cliecked X position < the rectangle's X. If caret rectangle's x bigger than mouse X you can try previous offset.
UPDATE2: You could override IconView and use paint() method to store the last painted rectangle for the image view. Store in a Map last painted rect. On mouse move/click check the map to find whether one of the rectangles contains the point.
OR
You can use View's method getChildAllocation() Something similar is described here to calculate Image bounds. Start from the root view and go down till the leaves calculating proper view for the X, Y. If the leaf view is IconView your are over image.