4

I have a problem with using JTextArea. My actual setup is different, but the effects remain. Here is an image of the problem:

enter image description here

The moment the owning JDialog resizes just 1 pixel below what the JTextArea's require for their preferred sizes, the text areas suddenly resize. In my actual setup, they suddenly grow in height. I am using a GridBagLayout, but it seems to happen in other layouts. Why is this?

Here is the easy-to-compile code to reproduce the above effect.

import java.awt.*;
import java.awt.event.*;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.swing.*;
import javax.swing.text.JTextComponent;

public class TextDemo extends JDialog implements ActionListener {
    private static final long serialVersionUID = -589374238138963529L;
    protected JTextField textField;
    protected JTextArea textArea;
    private final static String newline = "\n";

    private static final java.awt.Dimension SCREENSIZE = 
            java.awt.Toolkit.getDefaultToolkit().getScreenSize();
    private static final java.awt.Point SCREENCENTER =
        new java.awt.Point(SCREENSIZE.width/2,SCREENSIZE.height/2);

    public TextDemo(Window owner, String shortMessage, String message, JComponent accessory) {
        super(owner);

        setTitle("Test");
        setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);

        Icon icon = UIManager.getIcon("OptionPane.warningIcon");    

        JTextArea shortText = makeMultiLineLabel(true);
        shortText.setBorder(BorderFactory.createEtchedBorder());
        shortText.setFont(shortText.getFont().deriveFont(Font.BOLD));
        shortText.setRows(2);
        shortText.setColumns(20);
        shortText.setText(shortMessage); 

        JTextArea messageText = makeMultiLineLabel(true);
        messageText.setBorder(BorderFactory.createEtchedBorder());
        messageText.setFont(shortText.getFont().deriveFont(Font.PLAIN));
        messageText.setRows(4);
        messageText.setColumns(20);
        messageText.setText(message);

        JPanel buttonPanel = new JPanel();
        buttonPanel.add(new JButton("OK"));
        buttonPanel.add(new JButton("Cancel"));

        JPanel contentPanel = new JPanel();
        contentPanel.setOpaque(true);
        contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 8, 9));
        contentPanel.setLayout(new GridBagLayout());
        GridBagConstraints c;

        c = new GridBagConstraints();
        c.gridx = 0;
        c.gridy = 0;
        c.anchor = GridBagConstraints.FIRST_LINE_START;
        c.gridheight = 2;
        contentPanel.add(new JLabel(icon), c);

        c = new GridBagConstraints();
        c.gridx = 1;
        c.gridy = 0;
        c.fill = GridBagConstraints.BOTH;
        c.weighty = 1.0;
        c.weightx = 1.0;
        contentPanel.add(shortText, c);

        c = new GridBagConstraints();
        c.gridx = 1;
        c.gridy = 1;
        c.fill = GridBagConstraints.BOTH;
        c.weighty = 1.0;
        c.weightx = 1.0;
        contentPanel.add(messageText, c);

        if (accessory != null) {
            c = new GridBagConstraints();
            c.gridx = 0;
            c.gridy = 2;
            c.gridwidth = 2;
            c.fill = GridBagConstraints.BOTH;
            c.weighty = 1.0;
            c.weightx = 1.0;
            contentPanel.add(accessory, c);
        }
        c = new GridBagConstraints();
        c.gridwidth = 2;
        c.gridx = 0;
        c.gridy = 3;
        contentPanel.add(buttonPanel, c);

        setContentPane(contentPanel);
    }

    public void actionPerformed(ActionEvent evt) {
        String text = textField.getText();
        textArea.append(text + newline);
        textField.selectAll();

        //Make sure the new text is visible, even if there
        //was a selection in the text area.
        textArea.setCaretPosition(textArea.getDocument().getLength());
    }

    /**
     * Create the GUI and show it.  For thread safety,
     * this method should be invoked from the
     * event dispatch thread.
     */
    private static void createAndShowGUI() {
        //Create and set up the window.
        JFrame frame = new JFrame("TextDemo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        try {
            throw new Exception("Test");
        } catch (Exception e) {
            TextDemo t = new TextDemo(frame, "You won't get away with this!", 
                    "Alert! Alert! A chocy nut bar has been removed without payment!" +
                    " A chocy nut bar... has been REMOVED! WITHOUT PAYMENT! Alert, alert!",
                    getStackTraceTextArea(e));
            //Display the window.
            frame.pack();
            frame.setLocation(SCREENCENTER.x - frame.getSize().width/2,
                              SCREENCENTER.y - frame.getSize().height/2);
            frame.setVisible(true);

            t.setModal(true);
            t.pack();
            t.setLocation(getPos(t, t.getOwner()));
            t.setVisible(true);
        }
    }

    protected static JComponent getStackTraceTextArea(Throwable exception) {
        JTextArea textArea = new JTextArea();
        textArea.setEditable(false);
        textArea.setLineWrap(false);
        textArea.append(getTraceMessage(exception));
        textArea.setCaretPosition(0);
        JScrollPane scroll = new JScrollPane(textArea);
        scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
        scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        scroll.setPreferredSize(new Dimension(50, 140));
        return scroll;
    }

    private static final String getTraceMessage(Throwable exception) {
        StringBuilder out = new StringBuilder((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"))
                .format(new Date())+": Unhandled Exception: \n"
                +exception.toString()+"\n\nStack Trace:\n");
        StackTraceElement[] stackTrace = exception.getStackTrace();
        for (int i = 0; i < stackTrace.length; i++) {
            String toAppend = stackTrace[i].toString();
            if (i != stackTrace.length-1) toAppend += "\n";
            out.append(toAppend);
        }
        return out.toString();
    }

    public static final JTextArea makeMultiLineLabel(boolean selectable) {
        JTextArea area = new JTextArea();
        area.setWrapStyleWord(true);
        area.setLineWrap(true);
        area.setFont(UIManager.getFont("Label.font"));
        area.setEditable(false);
        area.setCursor(null);
        area.setOpaque(false);
        area.setFocusable(selectable);
        area.setAlignmentX(JTextComponent.LEFT_ALIGNMENT);
        area.setMinimumSize(new Dimension(0,0));
        return area;
    }

    private static Point getPos(JDialog d, Window w) {
        return new Point(w.getX()+(w.getWidth ()-d.getWidth ())/2,
                         w.getY()+(w.getHeight()-d.getHeight())/2);
    }

    public static void main(String[] args) {
        //Schedule a job for the event dispatch thread:
        //creating and showing this application's GUI.
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI();
            }
        });
    }
}

EDIT With some changes suggested implemented, the problem still remains:

enter image description here

bombax
  • 1,189
  • 8
  • 26
  • This is a side effect of the text areas minimum size – MadProgrammer Aug 23 '13 at 00:32
  • Thanks - want to elaborate? – bombax Aug 23 '13 at 00:36
  • 2
    I think this is better suited to: 1) `JOptionPane` rather than `JDialog` 2) (Possibly multiline) `JLabel` rather than `JTextArea`. 3) `GridLayout` (for equally sized labels) or `BoxLayout` or `BorderLayout` (for labels of different height) rather than `GridBagLayout` for arranging the labels. 4) `setLocationByPlatform(true)` for the frame, rather than putting it in the middle of the screen. 5) `setLocationRelativeTo(parent)` for the dialog instead of the centering. – Andrew Thompson Aug 23 '13 at 00:54
  • 1
    As far as I can see, your problem lies inside the last line of `makeMultiLineLabel(...)` method, where you using `setMinimumSize(0, 0)`. When you are using a `Layout Manager` always restraint yourself, from using any `setXxXMethod(...)` method. Let the `Layout` worry about such thingy. Don't use it until you are not playing with `GroupLayout/Box`. Just comment out the `setMinimumSize()` and you will see `JTextArea` accomodating the changes, with `Rows/Columns`(as already suggested in the answer) only :-) – nIcE cOw Aug 23 '13 at 01:27

1 Answers1

5

The problem you're witnessing is the GridBagLayout trying to deal with a situation where it's not able to honour the preferredSize of the component, it reverts to using the components minimumSize instead...

You could use the GridBagConstraints#weightx property to force the component to always fill it's columns width...

c = new GridBagConstraints();
c.gridx = 1;
c.gridy = 0;
c.fill = GridBagConstraints.BOTH;
c.weighty = 1.0;
c.weightx = 1.0;
contentPanel.add(shortText, c);

c = new GridBagConstraints();
c.gridx = 1;
c.gridy = 1;
c.fill = GridBagConstraints.BOTH;
c.weighty = 1.0;
c.weightx = 1.0;
contentPanel.add(messageText, c);

This won't stop it from shrinking, but will stop it from "snapping" from one size to another.

Try and avoid using setPreferredSize, check out Should I avoid the use of set(Preferred|Maximum|Minimum)Size methods in Java Swing? for more details. Instead use the rows and columns properties of the JTextArea...

shortText.setRows(2); // for example

Personally, I would also wrap the JTextArea's in a JScrollPane, then it becomes, slightly, less important about having enough room for each

Feed Back...

Now, the question is out of context, but it appears to me you are going to a lot of effort for little gain.

For example, instead, you could use JOptionPane and take advantage of Swing's HTML rendering capabilities...

enter image description here

import java.awt.EventQueue;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class OptionPaneTest {

    public static void main(String[] args) {
        new OptionPaneTest();
    }

    public OptionPaneTest() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                StringBuilder sb = new StringBuilder(128);
                sb.append("<html><b><p align=center>You won't get away with this!</p></b><br>");
                sb.append("Alert! Alert! A chocy nut bar has been removed without payment!");
                sb.append("<br>A chocy nut bar... has been REMOVED! WITHOUT PAYMENT! Alert, alert!");

                JOptionPane.showMessageDialog(null, sb.toString(), "Alert", JOptionPane.WARNING_MESSAGE);

            }
        });
    }        
}

Also...

I think you'll find using...

frame.setLocationRelativeTo(null);

Much easier and less time consuming then...

frame.setLocation(SCREENCENTER.x - frame.getSize().width / 2,
                    SCREENCENTER.y - frame.getSize().height / 2);

This would also mean that you can use t.setLocationRelativeTo(frame) as well...

Oh, also +1 for the Red Dwarf reference ;)

Updated from updates to the question

The solution is still (basically) the same, use JTextArea#setRows and JTextArea#setColumns...

Your code...

enter image description here

My Code...

enter image description here

JTextArea shortText = makeMultiLineLabel(true);
shortText.setBorder(BorderFactory.createEtchedBorder());
shortText.setFont(shortText.getFont().deriveFont(Font.BOLD));
//       FontMetrics fm = shortText.getFontMetrics(
//                shortText.getFont());
//        shortText.setPreferredSize(new Dimension(
//                Math.min(fm.stringWidth(shortMessage), 300),
//                fm.getHeight()));
shortText.setRows(2);
shortText.setColumns(20);
shortText.setText(shortMessage);

JTextArea messageText = makeMultiLineLabel(true);
messageText.setBorder(BorderFactory.createEtchedBorder());
messageText.setFont(shortText.getFont().deriveFont(Font.PLAIN));
//        fm = messageText.getFontMetrics(
//                messageText.getFont());
//        messageText.setPreferredSize(new Dimension(
//                Math.min(fm.stringWidth(message), 300),
//                fm.getHeight()));
messageText.setRows(4);
messageText.setColumns(20);
messageText.setText(message);

You may also want to take a look at SwingX's JXErrorDialog as well

Community
  • 1
  • 1
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • 1
    +1 for setRows() so the text area can determine its preferred size, or you can also use something like `new JTextArea(1, 20)` when you create the text area. – camickr Aug 23 '13 at 00:44
  • Thanks for the suggestions - I know about JOptionPane but I'm doing some experiments right now. Your code fixed the problem, but it turns out the actual problem was with the "accessory" component I have in my actual setup. I've edited my post with the new code - now the actual effects I'm seeing turn up. Thanks for any help! :) – bombax Aug 23 '13 at 00:54
  • @bombax It's good to experiment. Check updates, the answer doesn't really change... – MadProgrammer Aug 23 '13 at 01:04
  • Thanks a ton! However, the problem still remains, at least on my computer - try resizing the dialog, and you'll hopefully see the same problem I do. I updated my post with a picture. – bombax Aug 23 '13 at 01:12
  • 1
    Try removing the `weighty` constraints for the `shortText` and `messageText` components WHEN there is no `accessory` OR, remove the `weighty` constraints for the `shortText` and `messageText` components completely and when there is no `accessory` put in `JPanel` with a `weighty` of `1`... – MadProgrammer Aug 23 '13 at 01:14
  • Thanks! Will experiment, I'll let you know how it goes. – bombax Aug 23 '13 at 01:15
  • I couldn't get it to work the way I wanted, so I finally gave up and used nested `BoxLayout`s instead, which seemed to work. – bombax Aug 23 '13 at 01:53