Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create an inspecting properties window, button driven as a JDialog

What I asked originally didn't clearly state my question/problem, so I will explain it better. I have a JButton that sets a JDialog to visible. The JDialog has a WindowListener that sets it to NOT visible on the windowDeactivated() event, which is triggered anytime the user clicks outside of the dialog. The button ActionListener checks if the dialog isVisible, hides it if true, shows it if false.

windowDeactivated() will always trigger whether clicking on the button or not, as long as the user clicks outside the dialog. The problem I'm having is when the user clicks the button to close the dialog. The dialog is closed by the WindowListener and then the ActionListener tries to display it.

If windowDeactivated() doesn't setVisible(false), then the dialog is still open, but behind the parent window. What I'm asking for is how to get access to the location of the click inside windowDeactivated(). If I know that the user clicked on the button and windowDeactivated() can skip hiding the dialog, so that the button's ActionListener will see that it's still visible and hide it.

public PropertiesButton extends JButton {

    private JDialog theWindow;

    public PropertiesButton() {
        theWindow = new JDialog();
        theWindow.setUndecorated(true);
        theWindow.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        theWindow.add(new JMenuCheckBoxItem("Something"));
        theWindow.addWindowListener(new WindowListener() {
            // just an example, need to implement other methods
            public void windowDeactivated(WindowEvent e) {
                theWindow.setVisible(false);
            }
        });
        this.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (theWindow.isVisible()) {
                    theWindow.setVisible(false);
                } else {
                    JButton btn = (JButton)e.getSource();
                    theWindow.setLocation(btn.getLocationOnScreen.x,btn.getLocationOnScreen.x-50);
                    theWindow.setVisible(true);
                }
            }
        });
        theWindow.setVisible(false);
    }

}
like image 473
Brian Avatar asked Nov 16 '10 14:11

Brian


2 Answers

You could try using a JPanel instead of a a JDialog for the dropdown property list. Something like this:

public class PropertiesButton extends JButton {

    private JPanel theWindow;

    public PropertiesButton() {
        theWindow = new JPanel();
        theWindow.add(new JMenuCheckBoxItem("Something"));

        this.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (theWindow.isVisible()) {
                    theWindow.setVisible(false);
                    getParent().remove(theWindow);
                } else {
                    JButton btn = (JButton)e.getSource();
                    getParent().add(theWindow);             
                    theWindow.setBounds(
                       btn.getX(),
                       btn.getY() + btn.getHeight(), 100, 100);

                    theWindow.setVisible(true);
                }
            }
        });
        theWindow.setVisible(false);
    }

}

Using lightweight components instead of heavyweights ones like the JDialog is always preferable in Swing, and has less undesirable effects like the one you report. The only issue of this approach is that the panel position and size could be affected by the layout manager active in the parent.

like image 91
awheel Avatar answered Nov 03 '22 22:11

awheel


I was curious so I decided to have a try at this problem. As you found out, it's harder than it looks because whatever code you write in the WindowAdapter, this will always fire before the parent window and the button gets focus, and therefore the dialog will be closed already.

I believe the solution is to make sure the button is disabled until the dialog has been closed for a while, and that's what I've done. I disable the button while the dialog is closing. The second challenge was to find a way to enable the button again, but only after the mouse-down event has been processed, otherwise the button will be clicked and the dialog will show again immediately.

My first solution used a javax.swing.Timer which was set to trigger once upon the dialog losing focus, with a delay of 100ms, which would then re-enable the button. This worked because the small time delay ensured the button wasn't enabled until after the click event had already gone to the button, and since the button was still disabled, it wasn't clicked.

The second solution, which I post here, is better, because no timers or delays are required. I simply wrap the call to re-enable the button in SwingUtilities.invokeLater, which pushes this event to the END of the event queue. At this point, the mouse-down event is already on the queue, so the action to enable the button is guaranteed to happen after this, since Swing processed events strictly in order. The disabling and enabling of the button happens so suddenly that you are unlikely to see it happen, but it's enough to stop you from clicking the button until the dialog has gone.

The example code has a main method which puts the button in a JFrame. You can open the dialog and then make it lose focus by clicking the button or by clicking the window's title bar. I refactored your original code so that the button is only responsible for showing and hiding the specified dialog, so you can re-use it to show any dialog you wish.

import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;

public class QuickDialogButton extends JButton {

    private final JDialog dialog;

    public QuickDialogButton(String label, JDialog d) {
        super(label);

        dialog = d;

        dialog.addWindowListener(new WindowAdapter() {
            public void windowDeactivated(WindowEvent e) {
                // Button will be disabled when we return.
                setEnabled(false);
                dialog.setVisible(false);
                // Button will be enabled again when all other events on the queue have finished.
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        setEnabled(true);
                    }
                });
            }
        });

        addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                Component c = (Component) e.getSource();
                dialog.setLocation(c.getLocationOnScreen().x, c.getLocationOnScreen().y + c.getHeight());
                dialog.setVisible(true);
            }
        });
    }

    public static void main(String[] args) {
        JFrame f = new JFrame("Parent Window");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JDialog d = new JDialog(f, "Child Dialog");
        d.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
        d.add(new JCheckBox("Something"));
        d.setUndecorated(true);
        d.pack();

        f.add(new QuickDialogButton("Button", d));
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

}
like image 1
BoffinBrain Avatar answered Nov 03 '22 21:11

BoffinBrain