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() { 开发者_如何学Go // 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); } }
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.
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);
}
}
An easy, if somewhat hackish, way that may solve this, is to let the PropertiesButton have a boolean flag which indicates whether we should bother handling the next button action. We flip this flag if the dialog is hidden due to a windowDeactivated event.
public PropertiesButton extends JButton {
private JDialog theWindow;
private boolean ignoreNextAction;
(snip)
theWindow.addWindowListener(new WindowAdapter() {
@Override
public void windowDeactivated(WindowEvent e) {
ignoreNextAction = true;
theWindow.setVisible(false);
}
});
this.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (ignoreNextAction) {
ignoreNextAction = false;
return;
}
// ...normal action handling follows
}
});
Note that I'm not 100% comfortable with this trick: there may be some subtle case I missed where the approach fails.
Expanding on awheel's advice, I've written the following example which uses Swing's glass pane functionality. The approach is a bit messy, but that's not uncommon when you're trying something moderately advanced in Swing.
The idea is to display a transparent overlay panel (a glass pane covering the entire window contents) when one clicks the button, and dispose of it when the user either clicks anywhere in the window or presses a key.
On top of this glass pane, I display another JPanel ("popup") and try to position it just above the button that triggers its visibility.
This approach has one limitation you original dialog-based solution doesn't: whatever's drawn on top of the glass pane must fit inside the frame's content area (after all, it's not a window). That's why I in the code below do some adjustments to ensure that popup<'s coordinates are within the content pane's bounds (otherwise the JLabel would simply be cropped at the frame's edges).
It also has the limitation that mouse presses caught by the glass pane aren't delegated to any underlying components. So if you click a button while the glass pane is visible, the glass pane will go away but also consume the click, and the button you thought you clicked will not react. It is possible to go around this if one wishes, but then it gets even messier and I wanted to keep my example relatively simple. :-)
import java.awt.Color; import java.awt.Container; import java.awt.FlowLayout; import java.awt.KeyEventDispatcher; import java.awt.KeyboardFocusManager; import java.awt.Point; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRootPane; import javax.swing.SwingUtilities; import javax.swing.border.BevelBorder; import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; public class GlassPaneTest extends JFrame { public static class PropertiesButton extends JButton { /** The currently displayed glass pane. * Should be null if nothing is displayed. */ private JPanel theGlassPane; /** Root pane of connected window. Used to attach the glass pane. */ private final JRootPane rootPane; /** Content pane of the connected window. Used for coordinate calculation. */ private final Container contentPane; /* A "key hook" that allows us to intercept any key press when the glass pane is visible, * so we can hide the glass pane. */ private final KeyEventDispatcher keyHook = new KeyEventDispatcher() { public boolean dispatchKeyEvent(KeyEvent e) { if (theGlassPane == null || e.getID() != KeyEvent.KEY_PRESSED) { return false; } setGlassPaneVisible(false); return true; } }; public PropertiesButton(Window parentWindow) { if (!(parentWindow instanceof JFrame || parentWindow instanceof JDialog)) { throw new IllegalArgumentException("only JFrame or JDialog instances are accepted"); } if (parentWindow instanceof JDialog) { rootPane = ((JDialog) parentWindow).getRootPane(); contentPane = ((JDialog) parentWindow).getContentPane(); } else { rootPane = ((JFrame) parentWindow).getRootPane(); contentPane = ((JFrame) parentWindow).getContentPane(); } addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { setGlassPaneVisible(theGlassPane == null); } }); } private JPanel createGlassPane() { // Create the glass pane as a transparent, layout-less panel // (to allow absolute positioning), covering the whole content pane. // Make it go away on any mouse press. JPanel gp = new JPanel(); gp = new JPanel(); gp.setOpaque(false); gp.setLayout(null); gp.setBounds(contentPane.getBounds()); gp.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { setGlassPaneVisible(false); } }); // Create the "popup" - a component displayed on the transparent // overlay. JPanel popup = new JPanel(); popup.setBorder(new CompoundBorder( new BevelBorder(BevelBorder.RAISED), new EmptyBorder(5, 5, 5, 5))); popup.setBackground(Color.YELLOW); popup.add(new JLabel("Some info for \"" + getText() + "\".")); // Needed since the glass pane has no layout manager. popup.setSize(popup.getPreferredSize()); // Position the popup just above the button that triggered // its visibility. Point buttonLocationInContentPane = SwingUtilities.convertPoint(this, 0, 0, contentPane); int x = buttonLocationInContentPane.x; int horizOverlap = x + popup.getWidth() - contentPane.getWidth(); if (horizOverlap > 0) { x -= horizOverlap; } int y = buttonLocationInContentPane.y - popup.getHeight(); if (y < 0) { y = 0; } popup.setLocation(x, y); gp.add(popup); return gp; } private void setGlassPaneVisible(boolean visible) { KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager(); if (visible) { theGlassPane = createGlassPane(); rootPane.setGlassPane(theGlassPane); theGlassPane.setVisible(true); kfm.addKeyEventDispatcher(keyHook); } else { theGlassPane.setVisible(false); kfm.removeKeyEventDispatcher(keyHook); theGlassPane = null; } } } // A simple test program public GlassPaneTest() { setTitle("A glass pane example"); setLayout(new FlowLayout(FlowLayout.CENTER)); for (int i = 1; i <= 10; ++i) { PropertiesButton pb = new PropertiesButton(this); pb.setText("Properties button " + i); add(pb); } setSize(400, 300); } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { JFrame f = new GlassPaneTest(); f.setDefaultCloseOperation(EXIT_ON_CLOSE); f.setVisible(true); } }); } }
May I suggest that instead of using a WindowListener, you use a WindowStateListener, and then test the WindowEvent passed in for both WINDOW_DEACTIVATED and WINDOW_LOST_FOCUS. This should cover the possibility of the Dialog being behind the parent window.
Here's a working solution. Basically we want to avoid showing the window if it was just closed by clicking on the button which also deactivates and hides the window. The mouseDown and windowDeactivated are both processed on the same input event, although the event times differ slightly. The action time can be much later since it is generated on the mouseUp. Using WindowAdapter is convenient to WindowListener and using the @Override annotation is good to avoid not having stuff work because of a typo.
public class PropertiesButton extends JButton {
private JDialog theWindow;
private long deactivateEventTime = System.currentTimeMillis();
private long mouseDownTime;
public PropertiesButton(String text, final Frame launcher) {
super(text);
theWindow = new JDialog();
theWindow.getContentPane().add(new JLabel("Properties"));
theWindow.pack();
// theWindow.setUndecorated(true);
theWindow.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
// theWindow.add(new JMenuCheckBoxItem("Something"));
theWindow.addWindowListener(new WindowAdapter() {
// just an example, need to implement other methods
@Override
public void windowDeactivated(WindowEvent e) {
deactivateEventTime = EventQueue.getMostRecentEventTime();
theWindow.setVisible(false);
}
});
this.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
boolean alsoDeactivated = Math.abs(deactivateEventTime - mouseDownTime) < 100;
if (theWindow.isVisible()) {
theWindow.setVisible(false);
} else if (!alsoDeactivated) {
// JButton btn = (JButton)e.getSource();
// theWindow.setLocation(btn.getLocationOnScreen().x,btn.getLocationOnScreen().x+50);
theWindow.setVisible(true);
}
}
});
theWindow.setVisible(false);
}
public void processMouseEvent(MouseEvent event) {
if (event.getID() == MouseEvent.MOUSE_PRESSED) {
mouseDownTime = event.getWhen();
}
super.processMouseEvent(event);
}
}
精彩评论