Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

(Java Swing) How do I create a JTextPane with multiple icons on the same line?

As the question states, how do I successfully place multiple icons on the same line of text in a JTextPane? Every time I try to change the value of actionText, the results are very unpredictable. As an example, this is the kind of thing I'm trying to achieve :

Cool board game text I'd like to have

If I pass just the icon-tokens to create icons (" ", for example), they just stack on top of each other (or maybe not, hard to tell). If I put " , " or " and ", then the first fighter-icon appears on the first line, while the comma and other fighter-icon appears on the second line.

I am currently trying using a solution built upon the oracle tutorial for JTextPane : JTextPane tutorial. Here is the chunk of my code that creates my custom text panes.

public final class GameTextPaneFactory {

private static final String[] ADVENTURER_TOKENS = {"<FIGHTER>", "<CLERIC>", "<WIZARD>", "<ROGUE>"};
private static final int TEXT_PANE_WIDTH = 30;

public static JTextPane createActionTextPane(String actionText) {
    ArrayList<String>[] wordsAndStyles = parseActionText(actionText);

    JTextPane actionTextPane = new JTextPane();
    StyledDocument doc = actionTextPane.getStyledDocument();
    addStylesToDocument(doc);

    try {
        for (int i=0; i < wordsAndStyles[0].size(); i++) {
            doc.insertString(doc.getLength(), wordsAndStyles[0].get(i),
                             doc.getStyle(wordsAndStyles[1].get(i)));
        }
    } catch (BadLocationException ble) {
        System.err.println("Couldn't insert initial text into text pane.");
    }

    actionTextPane.setEditable(false);
    return actionTextPane;      
}

private static void addStylesToDocument(StyledDocument doc) {
    // TODO add images (styles) here
    Style def = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE);
    Style regular = doc.addStyle("regular", def);

    Style icons = doc.addStyle("fighterIcon", regular);
    StyleConstants.setAlignment(icons, StyleConstants.ALIGN_CENTER);
    ImageIcon fighterIcon = new ImageIcon("images/fighter_image.png", "fighter");
    StyleConstants.setIcon(icons, fighterIcon);

    icons = doc.addStyle("clericIcon", regular);
    StyleConstants.setAlignment(icons, StyleConstants.ALIGN_CENTER);
    ImageIcon clericIcon = new ImageIcon("images/cleric_image.png", "cleric");
    StyleConstants.setIcon(icons, clericIcon);

    icons = doc.addStyle("wizardIcon", regular);
    StyleConstants.setAlignment(icons, StyleConstants.ALIGN_CENTER);
    ImageIcon wizardIcon = new ImageIcon("images/wizard_image.png", "wizard");
    StyleConstants.setIcon(icons, wizardIcon);

    icons = doc.addStyle("rogueIcon", regular);
    StyleConstants.setAlignment(icons, StyleConstants.ALIGN_CENTER);
    ImageIcon rogueIcon = new ImageIcon("images/rogue_image.png", "rogue");
    StyleConstants.setIcon(icons, rogueIcon);
}

private static ArrayList<String>[] parseActionText(String text) {
    String[] words = text.split(" ");
    ArrayList<String> outputStrings = new ArrayList<String>();
    ArrayList<String> outputStyles = new ArrayList<String>();
    StringBuilder nextStringBuilder = new StringBuilder();
    int currentLineLength = TEXT_PANE_WIDTH;

    for(String word : words) {                      
        if(Arrays.asList(ADVENTURER_TOKENS).contains(word)) {
            if(nextStringBuilder.length() != 0) {
                outputStrings.add(nextStringBuilder.toString());
                outputStyles.add("regular");
                nextStringBuilder = new StringBuilder();
            }

            outputStrings.add(" "); // this is ignored, but cannot be empty
            switch(word) {
                case "<FIGHTER>":
                    outputStyles.add("fighterIcon");
                    break;
                case "<CLERIC>":
                    outputStyles.add("clericIcon");
                    break;
                case "<WIZARD>":
                    outputStyles.add("wizardIcon");
                    break;
                case "<ROGUE>":
                    outputStyles.add("rogueIcon");
                    break;
            }

            currentLineLength += 3; // an icon is about 3 characters in length
        } else {
            if(currentLineLength + word.length() + 1 > TEXT_PANE_WIDTH) {
                nextStringBuilder.append("\n");
                currentLineLength = 0;
            }

            nextStringBuilder.append(" " + word);
            currentLineLength += word.length() + 1;
        }
    }

    if(nextStringBuilder.length() != 0) {
        outputStrings.add(nextStringBuilder.toString());
        outputStyles.add("regular");
    }       
    @SuppressWarnings("unchecked")
    ArrayList<String>[] output = new ArrayList[2];
    output[0] = outputStrings;
    output[1] = outputStyles;
    return output;
}

}

If someone has a better solution I am all ears. Thanks!

like image 983
Billy M. Avatar asked Dec 25 '22 12:12

Billy M.


1 Answers

Try adding

outputStrings.add(" "); // this is ignored, but cannot be empty
outputStyles.add("regular");

After each new "adventurer" style

outputStrings.add(" "); // this is ignored, but cannot be empty
switch (word) {
    case "<FIGHTER>":
        outputStyles.add("fighterIcon");
        break;
    case "<CLERIC>":
        outputStyles.add("clericIcon");
        break;
    case "<WIZARD>":
        outputStyles.add("wizardIcon");
        break;
    case "<ROGUE>":
        outputStyles.add("rogueIcon");
        break;
}
outputStrings.add(" "); // this is ignored, but cannot be empty
outputStyles.add("regular");

Updated

I had a bit of a play around, seeing if I could get the formatting to look a little better, this is basically what I came up with...

Adventure

Instead of using styles, I basically insert the text and images directly to the text pane. There seems to be issues with like styles been set next to each other, so that instead what happens, they are basically merged into a single entry in the document, this would explain why you had issues with your styles. I had a similar issue with the icons for some reason, hence the reason why I had to create a new instance each time...

This is a bit rough and ready, but the basic idea is there. It basically uses the Regular Expression API to find all the matches of the "keywords", inserts the text before it and then inserts a special icon depending on the keyword...

public static JTextPane createActionTextPane(String actionText) {
    JTextPane actionTextPane = new JTextPane();
    actionTextPane.setOpaque(false);

    StyledDocument doc = actionTextPane.getStyledDocument();

    Pattern pattern = Pattern.compile("<FIGHTER>|<CLERIC>|<GOLD>");
    Matcher matcher = pattern.matcher(actionText);
    int previousMatch = 0;
    while (matcher.find()) {

        int startIndex = matcher.start();
        int endIndex = matcher.end();
        String group = matcher.group();

        String subText = actionText.substring(previousMatch, startIndex);
        if (!subText.isEmpty()) {
            actionTextPane.replaceSelection(subText);
        } 
        switch (group) {
            case "<FIGHTER>":
                actionTextPane.insertIcon(new ImageIcon("fifight.gif"));
                break;
            case "<CLERIC>":
                actionTextPane.insertIcon(new ImageIcon("mage.gif"));
                break;
            case "<GOLD>":
                actionTextPane.insertIcon(new ImageIcon("Gold.png"));
                break;
        }

        previousMatch = endIndex;

    }
    String subText = actionText.substring(previousMatch);
    if (!subText.isEmpty()) {
        actionTextPane.replaceSelection(subText);
    }

    actionTextPane.setEditable(false);
    return actionTextPane;
}

Now, frankly, I've not bothered about line widths or the like, and instead, used a JScrollPane and the JTextComponent's wrapping capabilities instead...but that's up to you...

like image 63
MadProgrammer Avatar answered Mar 23 '23 01:03

MadProgrammer