Aligning and Inlining components (or icons) in JTextPane
Asked Answered
C

2

6

I'm working on an application in Java which, among other things, is supposed to display the details of a Magic: The Gathering card in a text field (since I'm using Swing, this is currently a JTextPane).

Those details contain text and small icons, with some of those icons inlined with the text (so the text flows around them), and some icons, by my design, right-aligned with some left-aligned text in the same line.

I took an image from another application which uses a very similar design to the one I'm working on (although not in Java):

enter image description here

This is how it basically should look like.

Now, for the love of everything, I can't get that to work in a JTextPane.

I started with trying to do this with CSS, but found out that the JEditorPane and subclasses don't support the "float" attribute, so I tried it with using the Pane's StyledDocument instead.

I didn't get the first part to work (the icon and right-align text at the top always ignored their alignment and were placed directly after the left-aligned text in the line) at first, until I found this question.

It was suggested to use the following line of code:

pane.setEditorKit(new HTMLEditorKit());

which somehow indeed fixed my first issue.

But now I'm stuck at the second part, getting those icons in the second part inline with the text. Here's what I currently got:

enter image description here

What I've found is that for some reason, when you switch the JTextPane to html mode with an editor kit with the line of code above, inserting components just goes completely crazy.

Both the icons on the top (which I have merged into a single image actually), and the ones in the text below (not merged) are both inside of JLabels, but it doesn't matter if I add them as images or inside of JLabels. The images or labels are definitely not bigger than what you see there, I have no idea at all where the extra whitespace is coming from.

I found this question, and the answer suggest that this is some kind of bug or just weird behavior with the html mode of the JEditorPane.

If I remove the above code line again, I end up with my original problem:

enter image description here

Depending on where exactly the icons are in the text, I get all kinds of different weird results. I put together some more example pictures below:

enter image description here

So, how could I possibly fix this? The JTextPane is working fine for me, except for this part, but I could possibly use some other solution, as long as the end result still looks the same. Remember that I might want to add some other components (like a Button) in there, so I'd like to stick to something native to Swing if possible at all.

The user will not be able to edit the TextPane's contents, but I'd like to add an option later to copy all of the content in one click (so I'd rather stay with a text area).

Below, I have put together a (not really that minimal) working example for you to play around with:

(EDIT: Updated code at the bottom now! The old code is still there under the following link.)

http://pastebin.com/RwAdPCzb

The icons that I'm using are below. You'd need to rename them and change the path in the code.

enter image description here
enter image description here
enter image description here

Some things to notice:

  • In the beginning I styled the text using the `Style` class, as described in the "How to use Editor Panes and Text Panes" tutorial on the Oracle website (TextSamplerDemo.java, for your reference). With this, I wasn't even able to do the right-align part at the top. Strangely, when I used the `SimpleAttributeSet` class for styling instead, even with the very same settings, it works.
  • I tried different alignment options for both the text and the labels which contain the icons. Regardless of what options I used, there was no visible difference at all.

UPDATE 1:

After Sharcoux' answer, I have edited my code to have 2 JLabels above the actual JTextPane which hold the two lines that were supposed to have different alignings (a left- and a right-aligned part). The JTextPane doesn't use a HTMLEditorKit anymore now, and I use insertIcon() to insert the icons into the text.

This way, the icons are inserted (almost) correctly!
Image here:
enter image description here

However, there are two small things that I'm still not satisfied with:

First:
I need to put everything into a JScrollPane because the text in the TextPane is much longer in my actual application. Since I now have three components instead of just the TextPane, I needed to put everything into a JPanel and that into the ScrollPane.
However, if you do it like this, the JTextPane doesn't know that its with should not exceed the JScrollPane's anymore. It stopps wrapping text and just grows as big as the entire text.
I have opened a new question for this, since I feel that this is a basic issue of Swing and deserves its own question. If you want to help, here is the link:
JTextComponent inside JPanel inside JScrollPane

Second:
This is probably not possible, but I guess I'll ask anyway. The icons have the same baseline as the text when you add them this way. Is they any way to move them just a bit lower? 2-3 pixels, maybe? They would align much better with the text that way. Two pictures below.
This is how it looks now:
enter image description here
And this is how I would like it to look:
enter image description here

Maybe I can subclass and override some part of the JTextPane to move all icons that are rendered on it down a set pixel amount, or something like that?

For reference, here is also my new, updated code. I replaced the old one above with a pastebin.com link, if you still want to look at it.


UPDATE 2:

My first problem has already been eliminated! I updated the code below to reflect that, too.

My second question still stands!

Here's the new code:

import java.awt.EventQueue;
import java.awt.Graphics2D;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JScrollPane;
import javax.swing.Scrollable;
import javax.swing.JTextPane;
import javax.swing.JViewport;
 
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import javax.swing.ScrollPaneConstants;
 
public class MinimalExample extends JFrame {
 
    private JPanel contentPane;
    private JScrollPane scrollPane;
    private JTextPane textPane;
 
    // Setup some data for an example card:
    String name = "Absorb Vis";
    String set = "CON";
    String manaCost = "{6}{B}";
    String printedType = "Sorcery";
    String artist = "Brandon Kitkouski";
 
    String rulesText = "Target player loses 4 life and you gain 4 life.\n"
            + "Basic landcycling {1}{B} ({1}{B}, Discard this card: "
            + "Search your library for a basic land card, reveal it, and put "
            + "it into your hand. Then shuffle your library.)";
 
    HashMap<String, BufferedImage> manaSymbolImages;
    private ScrollablePanel textPanel;
    //private JPanel textPanel;
    private JPanel headlinesPanel;
    private JPanel firstHeadlinePanel;
    private JPanel secondHeadlinePanel;
    private JLabel titleLabel;
    private JLabel manaCostLabel;
    private JLabel typeLabel;
    private JLabel setLabel;
 
    public static void main(String[] args) {
 
        EventQueue.invokeLater(new Runnable() {
 
            public void run() {
 
                try {
                    MinimalExample frame = new MinimalExample();
                    frame.setVisible(true);
                }
                catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
 
    public MinimalExample() {
 
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setBounds(100, 100, 230, 400);
        contentPane = new JPanel();
        contentPane.setBackground(Color.WHITE);
        contentPane.setBorder(null);
        setContentPane(contentPane);
        /* HTMLEditorKit eKit = new HTMLEditorKit();
         * textPane.setEditorKit(eKit); HTMLDocument htmlDoc = (HTMLDocument)
         * textPane.getDocument(); htmlDoc.setPreservesUnknownTags(false); */
        contentPane.setLayout(new GridLayout(0, 1, 0, 0));
 
        textPanel = new ScrollablePanel();
        //textPanel = new JPanel();
        textPanel.setBackground(Color.WHITE);
        textPanel.setLayout(new BorderLayout(0, 0));
 
        headlinesPanel = new JPanel();
        headlinesPanel.setBorder(new EmptyBorder(2, 5, 3, 5));
        headlinesPanel.setBackground(Color.WHITE);
        textPanel.add(headlinesPanel, BorderLayout.NORTH);
        headlinesPanel.setLayout(new GridLayout(0, 1, 0, 0));
 
        firstHeadlinePanel = new JPanel();
        firstHeadlinePanel.setBorder(new EmptyBorder(0, 0, 3, 0));
        firstHeadlinePanel.setOpaque(false);
        headlinesPanel.add(firstHeadlinePanel);
        firstHeadlinePanel.setLayout(new BorderLayout(0, 0));
 
        titleLabel = new JLabel("");
        titleLabel.setFont(new Font("Tahoma", Font.BOLD, 12));
        firstHeadlinePanel.add(titleLabel, BorderLayout.WEST);
 
        manaCostLabel = new JLabel("");
        firstHeadlinePanel.add(manaCostLabel, BorderLayout.EAST);
 
        secondHeadlinePanel = new JPanel();
        secondHeadlinePanel.setBorder(null);
        secondHeadlinePanel.setOpaque(false);
        headlinesPanel.add(secondHeadlinePanel);
        secondHeadlinePanel.setLayout(new BorderLayout(0, 0));
 
        typeLabel = new JLabel("");
        typeLabel.setFont(new Font("Tahoma", Font.PLAIN, 12));
        secondHeadlinePanel.add(typeLabel, BorderLayout.WEST);
 
        setLabel = new JLabel("");
        setLabel.setFont(new Font("Tahoma", Font.BOLD, 12));
        secondHeadlinePanel.add(setLabel, BorderLayout.EAST);
 
        scrollPane = new JScrollPane();
        scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.setBackground(Color.WHITE);
        contentPane.add(scrollPane);
 
        textPane = new JTextPane();
        textPane.setBorder(new EmptyBorder(0, 3, 0, 3));
        textPane.setAlignmentY(0.3f);
        textPane.setEditable(false);
 
        textPanel.add(textPane, BorderLayout.CENTER);
 
        scrollPane.setViewportView(textPanel);
 
        loadManaCostIcons();
        setPaneText();
    }
 
    // This part inserts the text into the document of the text pane.
    public void setPaneText() {
 
        titleLabel.setText(name);
        manaCostLabel.setIcon(combineSymbols(manaCost));
        typeLabel.setText(printedType);
        setLabel.setText(set);
 
        StyledDocument textPaneDoc = textPane.getStyledDocument();
 
        SimpleAttributeSet defaultAtts = new SimpleAttributeSet();
        StyleConstants.setFontFamily(defaultAtts, "SansSerif");
        StyleConstants.setFontSize(defaultAtts, 12);
 
        SimpleAttributeSet rulesAtts = new SimpleAttributeSet(defaultAtts);
 
        SimpleAttributeSet artistAtts = new SimpleAttributeSet(defaultAtts);
        StyleConstants.setFontSize(artistAtts, 10);
 
        addTextWithSymbols(rulesText, rulesAtts);
 
        try {
 
            textPaneDoc.insertString(textPaneDoc.getLength(), artist, artistAtts);
        }
        catch (BadLocationException e) {
 
            e.printStackTrace();
        }
 
        textPane.revalidate();
        textPane.repaint();
    }
 
    /* This adds the rest of the text to the pane. The codes for the symbols get
     * replaced by the actual symbols and the text gets inserted piece by piece. */
    public void addTextWithSymbols(String text, SimpleAttributeSet style) {
 
        StyledDocument textPaneDoc = textPane.getStyledDocument();
        Pattern symbolPattern = Pattern.compile("\\{(.*?)\\}");
 
        try {
 
            Matcher symbolMatcher = symbolPattern.matcher(text);
            int previousMatch = 0;
 
            while (symbolMatcher.find()) {
 
                int start = symbolMatcher.start();
                int end = symbolMatcher.end();
                String subStringText = text.substring(previousMatch, start);
                String currentMatch = text.substring(start, end);
 
                if (subStringText.isEmpty() == false) {
 
                    textPaneDoc.insertString(textPaneDoc.getLength(), subStringText, style);
                }
 
                ImageIcon currentIcon = new ImageIcon(manaSymbolImages.get(currentMatch));
 
                SimpleAttributeSet iconAtts = new SimpleAttributeSet();
                JLabel iconLabel = new JLabel(currentIcon);
                StyleConstants.setComponent(iconAtts, iconLabel);
 
                textPane.insertIcon(currentIcon);
                previousMatch = end;
            }
 
            String subStringText = text.substring(previousMatch);
 
            if (subStringText.isEmpty() == false) {
               
                textPaneDoc.insertString(textPaneDoc.getLength(), subStringText + "\n", style);
            }
        }
        catch (Exception e) {
 
            e.printStackTrace();
        }
    }
 
    /* Everything below is more or less irrelevant. However, you might need to
     * adjust the image image file paths. */
 
    public void loadManaCostIcons() {
 
        manaSymbolImages = new HashMap<String, BufferedImage>();
        try {
 
            // Most likely, those paths won't work for you!
            File bFile = new File("resource/B.png");
            File c1File = new File("resource/1.png");
            File c6File = new File("resource/6.png");
 
            manaSymbolImages.put("{B}", ImageIO.read(bFile));
            manaSymbolImages.put("{1}", ImageIO.read(c1File));
            manaSymbolImages.put("{6}", ImageIO.read(c6File));
        }
        catch (IOException e) {
 
            e.printStackTrace();
        }
    }
 
    public ImageIcon combineSymbols(String symbols) {
 
        String[] manaSymbols = symbols.split("(?<=})");
        int combinedWidth = 0;
        int maxHeight = 0;
 
        for (int i = 0; i < manaSymbols.length; i++) {
 
            BufferedImage currentSymbolImage = manaSymbolImages.get(manaSymbols[i]);
            combinedWidth += currentSymbolImage.getWidth();
 
            if (maxHeight < currentSymbolImage.getWidth()) {
                maxHeight = currentSymbolImage.getWidth();
            }
        }
 
        BufferedImage combinedManaCostImage = new BufferedImage(combinedWidth, maxHeight,
                BufferedImage.TYPE_INT_ARGB);
        Graphics2D graphics = combinedManaCostImage.createGraphics();
 
        int currentPosition = 0;
 
        for (int i = 0; i < manaSymbols.length; i++) {
 
            BufferedImage tempCurrentImage = manaSymbolImages.get(manaSymbols[i].trim());
            graphics.drawImage(tempCurrentImage, null, currentPosition, 0);
            currentPosition += tempCurrentImage.getWidth();
        }
 
        graphics.dispose();
        return (new ImageIcon(combinedManaCostImage));
    }
 
    /* Original source of this is here:
     * https://mcmap.net/q/1135570/-jtextarea-on-jpanel-inside-jscrollpane-does-not-resize-properly
     * And one update to it is here:
     *  */
    private static class ScrollablePanel extends JPanel implements Scrollable {
 
        @Override
        public Dimension getPreferredScrollableViewportSize() {
 
            return super.getPreferredSize();
        }
 
        @Override
        public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation,
                int direction) {
 
            return 16;
        }
 
        @Override
        public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation,
                int direction) {
 
            return 16;
        }
 
        @Override
        public boolean getScrollableTracksViewportWidth() {
 
            return true;
        }
 
        @Override
        public boolean getScrollableTracksViewportHeight() {
 
            boolean track = true;
            Container parent = getParent();
            if (parent instanceof JViewport) {
 
                JViewport viewport = (JViewport) parent;
                if (viewport.getHeight() < getPreferredSize().height) {
                    track = false;
                }
 
            }
 
            return track;
        }
    }
}
Catalogue answered 8/9, 2015 at 20:34 Comment(2)
I added all the images but the last one to the questions. The last one link is not right if you updated the link. I will add it to the question.Entomologize
@Entomologize Thanks for your help! The last link was actually the link to an imgur.com album with the three icons I'm using. But they're so small, it's hard to see. I wish I could make attachments to the post... I edited the three single links in now!Catalogue
N
4

I think that the issue is the way you insert your images, and most probably, from your combineSymbol method.

Here is the way to insert stuff in the JTextPane :

public class Test {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame mainFrame = new JFrame("test");
                mainFrame.setSize(300, 100);
                mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                Container pane = mainFrame.getContentPane();
                pane.setLayout(new BorderLayout());

                JTP jtp = new JTP();
                pane.add(jtp);

                mainFrame.setVisible(true);
            }
        });
    }

    static class JTP extends JTextPane {

        JTP() {
            HTMLEditorKit eKit = new HTMLEditorKit();
            setEditorKit(eKit);
            HTMLDocument htmlDoc = (HTMLDocument) getDocument();//the HTMLEditorKit automatically created an HTMLDocument
            htmlDoc.setPreservesUnknownTags(false);//I advice you to put this line if you plan to insert some foreign HTML

            //inserting plain text (just change null for an attributeSet for styled text)
            try {
                htmlDoc.insertString(0, "test", null);
            } catch (BadLocationException ex) {
                Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
            }

            //inserting images
            insertIcon(new ImageIcon("image.png"));
            insertIcon(new ImageIcon("image.png"));

            //inserting components (With component, you should specify the yAlignment by yourself)
            JLabel label = new JLabel(new ImageIcon("image.png"));
            label.setAlignmentY(JLabel.TOP);
            insertComponent(label);
        }

    }

}

But to make things easier, I strongly advice you to use a title line outside the JTextPane. Text editors aren't really made for text having different alignment on the same line. Here is what I would suggest :

public class Test {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame mainFrame = new JFrame("test");
                mainFrame.setSize(300, 100);
                mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                Container pane = mainFrame.getContentPane();
                pane.setLayout(new BorderLayout());
                pane.setBackground(Color.WHITE);

                pane.add(new JTP());
                pane.add(new Title(), BorderLayout.NORTH);

                mainFrame.setVisible(true);
            }
        });
    }

    static class JTP extends JTextPane {

        JTP() {
            setEditable(false);
            setOpaque(false);

            HTMLEditorKit eKit = new HTMLEditorKit();
            setEditorKit(eKit);
            HTMLDocument htmlDoc = (HTMLDocument) getDocument();//the HTMLEditorKit automatically created an HTMLDocument
            htmlDoc.setPreservesUnknownTags(false);//I advice you to put this line if you plan to insert some foreign HTML

            //inserting plain text (just change null for an attributeSet for styled text)
            try {
                htmlDoc.insertString(0, "capacity : ", null);
            } catch (BadLocationException ex) {
                Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
            }

            //inserting images
            insertIcon(new ImageIcon("image.png"));
            insertIcon(new ImageIcon("image.png"));

            //inserting components (With component, you should specify the yAlignment by yourself)
            JLabel label = new JLabel(new ImageIcon("image.png"));
            label.setAlignmentY(JLabel.TOP);
            insertComponent(label);
        }

    }

    static class Title extends JPanel {

        Title() {
            setLayout(new BorderLayout());
            setOpaque(false);
            add(new JLabel("<html><b>Card title</b></html>"), BorderLayout.CENTER);
            add(new JLabel(new ImageIcon("image.png")), BorderLayout.EAST);
        }

    }

}
Nefertiti answered 9/9, 2015 at 13:40 Comment(3)
Thanks for your help! I went with the second solution, since you're right that having two types of alignment in the same line isn't something text editors are supporting correctly normally. I still have some questions left, and updated my question accordingly. I will accept your answer, but if you'd still take a look at those new questions, I'd be very happy. ;)Catalogue
For your 1st Q, you found an answer. Though, as a Magic player, I would suggest you to keep the title line outside of the JScrollPane. This way, the user can always see the major information. For the second question, have a better look at my answer and tell me that I didn't spend time writing comments for nothing... ;)Nefertiti
For the first question, yes, I'm still thinking about it. I might change it to having it outside in the end. ;) For the second one... I'm sorry. I just derped. I read your comments, and I tried with JLabels and JLabel.setAlignmentY(), but for some reason I only tried the JLabel alignment constants (none of which suited me) as parameters. I didn't even get the idea to just use some other float values, like 0.9f. Well I did now, and it works perfectly. Thanks for being patient with me! :)Catalogue
T
1

You can try to define your own TabStops to align icons.

If you know size of icon and width of JTextPane just add your content as "Person Name -tab- icon" and set custom TabSet for the paragraph. The TabSet has just one TabStop. The TabStop position = jTextPaneWidth - iconWidth

Tumbrel answered 9/9, 2015 at 12:6 Comment(1)
Hi, thanks for your help! Your solution sounds interesting, but I'm afraid you'd need to go into a little more detail. I get what you're saying, but I'm not sure how I would implement it correctly. If you'd update your answer, I'd be very grateful!Catalogue

© 2022 - 2024 — McMap. All rights reserved.