Java Forum / GUI / February 2005
Problems combining a JToggleButton and a JPopupMenu
Stephen Riehm - 13 Feb 2005 21:51 GMT Hi,
I've got a JToggleButton (always visible) and I'm using its actionlistener to popup a JPopupMenu. The JButton then listens for the JPopupMenu's popupMenuWillBecomeInvisible() event so that it can reset itself when the menu disappears. This works fine, except if the user clicks on the JToggleButton to close the popup. In this case there's a race condition: first the popup loses focus and closes itself, which triggers the willBecomeInvisble event, which resets the toggle button... AND THEN the togglebutton's action fires, and it always thinks the button is being activated. The result for the user is that the menu pop's away and comes straight back again.
Is there a common pattern for connecting a popup to a toggle button so that the button can be used for opening and closing the popup? (without sacrificing the normal closing of the popup when the focus is taken away)
Thanks,
Rom
Andrey Kuznetsov - 13 Feb 2005 23:21 GMT > I've got a JToggleButton (always visible) and I'm using its actionlistener > to popup a JPopupMenu. The JButton then listens for the JPopupMenu's [quoted text clipped - 10 lines] > (without sacrificing the normal closing of the popup when the focus is > taken away) common pattern is to reset state of button a little bit later. i.e. SwingUtilities.invokeLater(new Runnable() { public void run() { //reset state here } });
 Signature Andrey Kuznetsov http://uio.dev.java.net Unified I/O for Java http://reader.imagero.com Java image reader http://jgui.imagero.com Java GUI components and utilities
Stephen Riehm - 14 Feb 2005 22:05 GMT >>Is there a common pattern for connecting a popup to a toggle button so >>that the button can be used for opening and closing the popup? [quoted text clipped - 8 lines] > } > }); hmm... thanks for the tip! I tried it, but then I got a non-deterministic reaction. (before it was deterministically wrong). Only about one click in five actually has the desired result, the rest behave as it did in my original post. What I really want is to get the button's action before the popup closes, or, to prevent the popup from closing / triggering the button reset if and only if the mouse is over the button when clicked.
Does anyone else have an idea for connecting a popup and a toggle button tegether properly?
Thanks in advance,
Steve
Andrey Kuznetsov - 15 Feb 2005 00:35 GMT > I tried it, but then I got a non-deterministic reaction. (before it was > deterministically wrong). Only about one click in five actually has the > desired result, the rest behave as it did in my original post. please post SSCCE (http://www.physci.org/codes/sscce.jsp)
 Signature Andrey Kuznetsov http://uio.dev.java.net Unified I/O for Java http://reader.imagero.com Java image reader http://jgui.imagero.com Java GUI components and utilities
stephen.riehm@gmx.net - 15 Feb 2005 09:52 GMT > > I tried it, but then I got a non-deterministic reaction. (before it was > > deterministically wrong). Only about one click in five actually has the > > desired result, the rest behave as it did in my original post. > > please post SSCCE (http://www.physci.org/codes/sscce.jsp) Gladly. (see below) The following displays the problem on linux and mac. I don't have a windows box :-) What I said about using invokeLater doesn't apply to this example though. Maybe it's too small, but as you can see, using invokeLater or not makes effectively no difference.
Thanks,
Steve ----- PopupButton.java import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*;
/* * Standalone test of connecting a JToggleButton with a JPopupMenu. * The goal is to have the toggle appear selected only while the popup * is visible. When the popup is open, the user should be able to close * the popup by: * 1. clicking in the popup (a selection) * 2. clicking anywhere outside the popup (cancel) * 3. clicking on the toggle button again (raising the button and * canceling the popup) * Everything works except the case 3. * In this case the following happen: * Initial state: button is selected, popup is open * The user clicks the toggle button * The popup loses focus * The popup fires a cancel event * The popup fires an isClosing event * The popup becomes invisible * The button's action event is fired * => the button is not selected and the popup is closed * Problem: The action event NEVER sees the popup as being open or * the button as being selected */ public class PopupButton extends JFrame implements ActionListener, PopupMenuListener {
// start the test. Use any argument on the command line to use // invokeLater() to update the toggle button instead of directly // updating its status. public static void main( String[] args ) { final boolean useInvokeLater = ( args.length > 0 ); javax.swing.SwingUtilities.invokeLater( new Runnable() { public void run() { new PopupButton( useInvokeLater ); } } ); }
public PopupButton( boolean useInvokeLater ) { this.useInvokeLater = useInvokeLater;
setDefaultCloseOperation( EXIT_ON_CLOSE ); getContentPane().setLayout( new FlowLayout() );
button = new JToggleButton("click me"); // events caught by actionPerformed() below button.addActionListener( this );
popup = new JPopupMenu(); // events caught by popupMenuWillBecomeInvisible() below popup.addPopupMenuListener( this );
response = new JButton( new AbstractAction("thank you") { public void actionPerformed( ActionEvent e ) { popup.setVisible( false ); } } );
getContentPane().add( new JLabel( useInvokeLater ? "using invokeLater" : "using direct updates" ) ); getContentPane().add( button ); getContentPane().add( new JLabel( "click here to close the popup " ) ); popup.add( response );
pack(); setVisible( true ); }
// respond to the button's click actions by opening or closing the // popup public void actionPerformed( ActionEvent e ) { if( button.isSelected() ) { Dimension d = button.getPreferredSize(); popup.show( button, 0, d.height ); } else { popup.setVisible( false ); } }
// required by the PopupMenuListener interface // disable the toggle button when the popup closes public void popupMenuWillBecomeInvisible( PopupMenuEvent e ) { if( useInvokeLater ) { javax.swing.SwingUtilities.invokeLater( new Runnable() { public void run() { button.setSelected( false ); } } ); } else { button.setSelected( false ); } }
// required by the PopupMenuListener interface public void popupMenuCanceled( PopupMenuEvent e ) {}
// required by the PopupMenuListener interface public void popupMenuWillBecomeVisible( PopupMenuEvent e ) {}
//------------------------------------------------------------ // ATTRIBUTES //------------------------------------------------------------ private JToggleButton button; // button to open popup private JPopupMenu popup; // popup panel private JButton response; // button in popup panel private boolean useInvokeLater; }
Andrey Kuznetsov - 15 Feb 2005 15:06 GMT I changed a bit class design, because extending JFrame is not the best... Most important thing is to check time since last menu closing, if it is too short, then actionEvent should be ignored.
import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener;
public class PopupButton extends JToggleButton implements ActionListener {
// start the test. Use any argument on the command line to use // invokeLater() to update the toggle button instead of directly // updating its status. public static void main(String[] args) { JFrame frame = new JFrame("PopupButton test");
final PopupButton popup = new PopupButton("PopupButton test"); JButton response = new JButton( new AbstractAction("thank you") { public void actionPerformed(ActionEvent e) { popup.getPopup().setVisible(false); popup.setSelected(false); } } ); popup.getPopup().add(response); frame.getContentPane().add(popup, BorderLayout.NORTH); frame.getContentPane().add(new JLabel("PopupTest"), BorderLayout.CENTER); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); frame.show(); }
public PopupButton(String text) { super(text); // events caught by actionPerformed() below addActionListener(this); popup = new JPopupMenu(); popup.addPopupMenuListener(new PopupMenuListener() { public void popupMenuWillBecomeVisible(PopupMenuEvent e) { }
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { setSelected(false); lastEvent = System.currentTimeMillis(); }
public void popupMenuCanceled(PopupMenuEvent e) { } }); }
public JPopupMenu getPopup() { return popup; }
// respond to the button's click actions by opening or closing the popup public void actionPerformed(ActionEvent e) { //check time when popup menu was made invisible if ((System.currentTimeMillis() - lastEvent) < 250) { setSelected(false); return; }
if (isSelected()) { Dimension d = getPreferredSize(); popup.show(PopupButton.this, 0, d.height); } else { popup.setVisible(false); } }
long lastEvent; private JPopupMenu popup; // popup panel }
 Signature Andrey Kuznetsov http://uio.dev.java.net Unified I/O for Java http://reader.imagero.com Java image reader http://jgui.imagero.com Java GUI components and utilities
Andrey Kuznetsov - 15 Feb 2005 15:08 GMT > import javax.swing.*; > import java.awt.*; > import java.awt.event.ActionEvent; > import java.awt.event.ActionListener; > import java.beans.PropertyChangeEvent; > import java.beans.PropertyChangeListener; hmm, here is correct imports:
import javax.swing.*; import javax.swing.event.PopupMenuListener; import javax.swing.event.PopupMenuEvent; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener;
 Signature Andrey Kuznetsov http://uio.dev.java.net Unified I/O for Java http://reader.imagero.com Java image reader http://jgui.imagero.com Java GUI components and utilities
stephen.riehm@gmx.net - 17 Feb 2005 10:34 GMT > I changed a bit class design, because extending JFrame is not the best... umm... you wanted a short, self contained, correct example - the original code has nothing to do with jframes, I just wrapped up the important bits in a frame to meet the requirements :-)
> Most important thing is to check time since last menu closing, > if it is too short, then actionEvent should be ignored. Thanks. I thought of doing that, but I really don't like using timing for things like this. The reason being that this is a sequence of events. If the user is on a slow, overloaded machine, any time limit you put in can be too short. Making the time limits longer only makes the interface slow. In this case it would result in the button appearing depressed even though the popup menu is closed, in other situations this sort of thing can lead to really nasty side effects. But since I haven't found a better solution yet, I guess I'll have to do something like this.
Thanks for your help!
Steve
Thomas Weidenfeller - 17 Feb 2005 11:16 GMT > I changed a bit class design, because extending JFrame is not the best... It often is. If you want to produce a re-usable component (also known as a JavaBean), then for practical reasons you better subclass. Otherwise you have to duplicate too many methods to provide the existing (and thus expected) behavior plus your own new behavior. Also, by subclassing you can use your component directly everywhere where the superclass can be used.
/Thomas
 Signature The comp.lang.java.gui FAQ: ftp://ftp.cs.uu.nl/pub/NEWS.ANSWERS/computer-lang/java/gui/faq
Andrey Kuznetsov - 19 Feb 2005 22:19 GMT >> I changed a bit class design, because extending JFrame is not the best... > [quoted text clipped - 4 lines] > can use your component directly everywhere where the superclass can be > used. hmm, my english is not the best... I mean of course: extending of JFrame is not the best way if you want to customize JToggleButton...
 Signature Andrey Kuznetsov http://uio.dev.java.net Unified I/O for Java http://reader.imagero.com Java image reader http://jgui.imagero.com Java GUI components and utilities
John McGrath - 15 Feb 2005 16:14 GMT > // required by the PopupMenuListener interface > // disable the toggle button when the popup closes [quoted text clipped - 12 lines] > } > } That problem looks *very* familiar. As I recall, the real problem was in unselecting the JToggleButton when the menu is closed as the result of a focus loss or of selecting a menu item. I could not find a way to detect this condition in such a way that it was not also triggered when you pressed the toggle button to close the menu. If the code deselected the JToggleButton when the menu closed, that occurred *before* the button handling, so that ended up selecting the JToggleButton again.
What I did to deal with this was to just use a plain old JButton, so I did not have to deal with the JToggleButton selection.
 Signature Regards,
John McGrath
John McGrath - 16 Feb 2005 05:47 GMT I just found a way to deal with this in my PopupButton class. Instead of doing a setSelected( false ) when the menu is closing, I used the following code:
SwingUtilities.invokeLater( new Runnable() { public void run() { doClick(); } } );
If the button is already armed and pressed, this has no effect, so it does not interfere with the user pressing the button. And if called when the menu is closed for some other reason, the doClick() deselects the button.
 Signature Regards,
John McGrath
stephen.riehm@gmx.net - 17 Feb 2005 11:02 GMT hmmm.... we're getting closer! This works about 70% of the time - and when it doesn't work the popup re-opens.
What I don't understand, is why there isn't a deterministic way of doing this? Using invokeLater is also really a hack to click the button after a short non-deterministic pause.
Thanks just the same. I'll put this and the timestamp logic from Andrey together and it should be OK.
Cheers,
Steve
John McGrath - 19 Feb 2005 18:03 GMT > hmmm.... we're getting closer! This works about 70% of the time - and > when it doesn't work the popup re-opens. Interesting. I made that change in a PopupButton class that I wrote a while back, and it seems pretty solid, although I have not yet tested it on systems other than Windows.
Can you post the version of your code with the doClick() that works 70% of the time. I will take a look at it to see if I can tell why it does not work consistently and my code does. I will also work up a similar example using my code.
> What I don't understand, is why there isn't a deterministic way of > doing this? Using invokeLater is also really a hack to click the > button after a short non-deterministic pause. I am not sure why, but there have been issues with popups, particularly on other platforms going back to the beginning of Swing. I suspect that has had something to do with it.
 Signature Regards,
John McGrath
stephen.riehm@gmx.net - 22 Feb 2005 16:59 GMT Hi John,
OK, here's the whole thing again, with all 4 different cases built in. Running it with no argument or 0 gives you my original code, 1 uses Andrey's time-check, 2 uses the invokeLater() method to trigger the button when the popup closes and 3 uses doClick(). My results are that it behaves the same for case 0 and 2 (broken) and properly with the time check. Although the doClick() option almost worked in the real code, in this example it throws a null pointer exception - haven't been able to figure out why yet :-( (btw. I get the exact same behaviour on OS-X and RedHat linux)
Thanks again,
Steve ---- cut here ---- import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*;
/* * Standalone test of connecting a JToggleButton with a JPopupMenu. * The goal is to have the toggle appear selected only while the popup * is visible. When the popup is open, the user should be able to close * the popup by: * 1. clicking in the popup (a selection) * 2. clicking anywhere outside the popup (cancel) * 3. clicking on the toggle button again (raising the button and * canceling the popup) * Everything works except the case 3. * In this case the following happen: * Initial state: button is selected, popup is open * The user clicks the toggle button * The popup loses focus * The popup fires a cancel event * The popup fires an isClosing event * The popup becomes invisible * The button's action event is fired * => the button is not selected and the popup is closed * Problem: The action event NEVER sees the popup as being open or * the button as being selected * * Usage: * java PopupButton [0-3] * * Arguments: * 0: display the original state - toggle button doesn't toggle popup * 1: use a delay to prevent triggering the toggle button twice * with one click * 2: use invokeLater to delay triggering the toggle button * 3: use doClick() to reset the button instead of setSelected() */ public class PopupButton extends JFrame implements ActionListener, PopupMenuListener {
// start the test. Use any argument on the command line to use // invokeLater() to update the toggle button instead of directly // updating its status. public static void main( String[] args ) { int testNum = 0; if( args.length > 0 ) { testNum = Integer.parseInt( args[0] ); } final int testCase = testNum; javax.swing.SwingUtilities.invokeLater( new Runnable() { public void run() { new PopupButton( testCase ); } } ); }
public PopupButton( int testCase ) { this.testCase = testCase;
setDefaultCloseOperation( EXIT_ON_CLOSE ); getContentPane().setLayout( new FlowLayout() );
button = new JToggleButton("click me"); // events caught by actionPerformed() below button.addActionListener( this );
popup = new JPopupMenu(); // events caught by popupMenuWillBecomeInvisible() below popup.addPopupMenuListener( this );
response = new JButton( new AbstractAction("thank you") { public void actionPerformed( ActionEvent e ) { popup.setVisible( false ); } } );
String testCaseNames[] = { "broken triggers", "timed triggers", "triggering via invokeLater()", "triggering via doClick()" }; getContentPane().add( new JLabel( "Testing " + testCaseNames[testCase] ) ); getContentPane().add( button ); getContentPane().add( new JLabel( "click here to close the popup " ) ); popup.add( response );
pack(); setVisible( true ); }
// respond to the button's click actions by opening or closing the // popup public void actionPerformed( ActionEvent e ) { switch( testCase ) { case 1: // ensure a minimum delay before allowing another event if ((System.currentTimeMillis() - lastEvent) < 250) { // lastEvent = System.currentTimeMillis(); button.setSelected( false ); System.out.println( "button action: double-event, deselecting button" ); return; } else { System.out.println( "button action: double-event time expired" ); lastEvent = System.currentTimeMillis(); } break; default: }
if( button.isSelected() ) { System.out.println( "button action: new popup" ); Dimension d = button.getPreferredSize(); popup.show( button, 0, d.height ); } else { System.out.println( "button action: closing old popup" ); popup.setVisible( false ); } }
// required by the PopupMenuListener interface // disable the toggle button when the popup closes public void popupMenuWillBecomeInvisible( PopupMenuEvent e ) { switch( testCase ) { case 1: System.out.println( "popup closing: deselecting button, catching event time" ); lastEvent = System.currentTimeMillis(); button.setSelected( false ); break; case 2: // use invokeLater() System.out.println( "popup closing: deselecting button later" ); javax.swing.SwingUtilities.invokeLater( new Runnable() { public void run() { button.setSelected( false ); } } ); break; case 3: // use doClick() System.out.println( "popup closing: clicking button" ); button.doClick(); break; default: // broken System.out.println( "popup closing: deselecting button" ); button.setSelected( false ); break; } }
// required by the PopupMenuListener interface public void popupMenuCanceled( PopupMenuEvent e ) {}
// required by the PopupMenuListener interface public void popupMenuWillBecomeVisible( PopupMenuEvent e ) {}
//------------------------------------------------------------ // ATTRIBUTES //------------------------------------------------------------ private JToggleButton button; // button to open popup private JPopupMenu popup; // popup panel private JButton response; // button in popup panel private int testCase; // which test case to run private long lastEvent; // prevent the button being pressed too quickly }
John McGrath - 23 Feb 2005 07:24 GMT > Although the doClick() option almost worked in the real code, in this > example it throws a null pointer exception - haven't been able to > figure out why yet :-( You left out the EventQueue.invokeLater().
case 3: // use doClick() System.out.println( "popup closing: clicking button" ); EventQueue.invokeLater( new Runnable() { public void run() { button.doClick(); } } ); break;
When I add that, it works very consistently for me. How does it work for you with the above changes?
> btw. I get the exact same behaviour on OS-X and RedHat linux What machines (speed/ram) are you using to test this? I tried my code on three different systems using JDK 1.5:
Window XP (Dell Precision Workstation 260, 2.8 GHz, 1 GB RAM) Fedora 3 (Dell PowerEdge 700 server, 2.8 GHz, 1 GB RAM) Red Hat 9 (Dell Dimension XPST, 450 MHz, 500 MB RAM)
I will pare my code down to a simpler program and post it shortly.
 Signature Regards,
John McGrath
John McGrath - 23 Feb 2005 09:18 GMT > I will pare my code down to a simpler program and post it shortly. OK, here it is. I just noticed that you were using a JButton rather than a JMenuItem, so I added both.
I ran this on all three systems, using both JDK 1.4 and JDK 1.5. I think I may have seen it "skip" once, but it generally ran correctly. I did notice a minor difference in behavior between JDK 1.4 and JDK 1.5, though. If you click on one popup button when the popup for another popup button is showing, the old popup goes away in both cases, but the new popup is only shown under JDK 1.5.
After thinking about this a little more, I suspect that there is a timing problem. Looking at the AbstractButton code again, I think I was wrong when I figured that the doClick() would always work. Part of what doClick() does is introduce a 68 ms delay, and I think that is what was making it work. This has the same effect as the 250 ms delay that Andrey suggested, except it looks like 68 ms is a little short for some systems.
In any case, here is the code. Would you run it on your systems and let me know how it works?
======================================================================= import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*;
public class PopupButton extends JToggleButton { private JPopupMenu popup;
public PopupButton( String text, JPopupMenu popup ) { super( text ); addActionListener( buttonAction ); setPopup( popup ); }
public void setPopup( JPopupMenu newPopup ) { if ( popup != null ) { popup.removePopupMenuListener( popupListener ); } popup = newPopup; if ( popup != null ) { popup.addPopupMenuListener( popupListener ); } }
protected void showPopup() { if ( popup != null ) { Point pos = new Point( 0, getSize().height ); popup.show( PopupButton.this, pos.x, pos.y ); } }
protected void hidePopup() { if ( popup != null ) { popup.setVisible( false ); } }
private ActionListener buttonAction = new ActionListener() { public void actionPerformed( ActionEvent event ) { if ( isSelected() ) { showPopup(); } else { hidePopup(); } } };
private PopupMenuListener popupListener = new PopupMenuListener() { public void popupMenuWillBecomeInvisible( PopupMenuEvent event ) { SwingUtilities.invokeLater( new Runnable() { public void run() { doClick(); } } ); }
public void popupMenuWillBecomeVisible( PopupMenuEvent event ) {} public void popupMenuCanceled( PopupMenuEvent event ) {} };
public static void main( String[] args ) { EventQueue.invokeLater( new Runnable() { public void run() { JFrame frame = new JFrame(); frame.setDefaultCloseOperation( javax.swing.JFrame. EXIT_ON_CLOSE );
// Popup menu JPopupMenu popup1 = new JPopupMenu(); popup1.add( "thank you" );
// Popup with button JPopupMenu popup2 = new JPopupMenu(); final PopupButton popupButton2 = new PopupButton( "click me", popup2 ); popup2.add( new JButton( new AbstractAction("thank you") { public void actionPerformed( ActionEvent event ) { popupButton2.hidePopup(); } } ) );
Container content = frame.getContentPane(); content.setLayout( new FlowLayout() ); content.add( new JLabel( "Testing PopupButton" ) ); content.add( new PopupButton( "click me", popup1 ) ); content.add( popupButton2 ); content.add( new JLabel("click here to close the popup") );
frame.pack(); frame.setVisible( true ); } } ); } } =======================================================================
 Signature Regards,
John McGrath
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|