Prevent CMenu action of DefaultSingleCDockable from closing the popup

Hi,

I have a single dockable to which I added a CMenu as action (via DefaultCDockable.addAction(CAction cMenu)). This CMenu itself contains several entries, including a few more CMenus which contain a couple of CRadioButtons or CCheckBoxes. So basically I got the following snippet (give or take):

    protected void action() { // some action }
}

CMenu radioMenu = new CMenu("Radio Menu", null);
radioMenu.add(new CRadioButton("Radio 1", null) {
    protected void changed() { // some action }
}
radioMenu.add(new CRadioButton("Radio 2", null) {
    protected void changed() { // some action }
}

CMenu checkMenu = new CMenu("Check Menu", null);
checkMenu.add(new CCheckBox("Checkbox 1", null) {
    protected void changed() { // some action }
}
checkMenu.add(new CCheckBox("Checkbox 2", null) {
    protected void changed() { // some action }
}

CMenu menu = new CMenu("View", IconLibrary.menuIcon);
menu.add(button1);
menu.add(radioMenu);
menu.add(checkmenu);

DefaultSingleCDockable dockable = new DefaultSingleCDockable(1, "Dockable", menu);

The dockable works perfectly (even in multi dockable setup), the menu is supposed to mimic something like the default JMenu (the menuIcon is a downward facing arrow). However, every time I change the selection of the radio button (or check / uncheck one of the checkboxes) within the CMenu, the CMenu is closed. This, of course, mimics the normal JMenu function as well. However, if the number of entries in either the checkbox or the radiobutton submenu is large, having to reopen the CMenu every time and navigate to the correct submenu is very clumsy and destroyes the user experience.

I have tried hooking in the changed(…) methods of the CRadioButtons/CCheckBoxes to prevent the CMenu from closing, but without any success. Could you give me a hint on how to get the CMenu to stay opened (or how I could (in code) reopen the CMenu again at the same position after the mouse action has been processed)? Any help is greatly appreciated!

Thanks in advance,
Christian

Can you keep a normal JMenu/JPopupMenu open? Because if that fails, you are out of luck anyways (and as far as I know, keeping a JMenu open is not easy).

Afterwards: I guess it depends on the code you need to keep a normal menu open - whether it is something that applies directly to the menu, or to the menu-items. But in any case: a CAction internally points to a DockAction, which contains a factory method “createView” which creates a “MenuViewItem” which then wraps around the JCheckBox/JRadioButton that is actually shown. There is probably not yet an easy way to override this mechanism, I’ll have to update the API a bit.

I propose you first show me how you would keep open an ordinary JPopupMenu or JMenu, and then I take your code and write a little example that is using it.

Hi Beni,

for a JMenu there do exist both possibilities I mentioned to keep the menu open, the one that prevents hiding is a bit clumsy, though, and can mess up the active look and feel. For the „reopening“-way of tackling the problem, see the following example. It uses the Swing MenuSelectionManager to get the current selected path and reopens the menu at the same position again. I use a JCheckBoxMenuItem for the example, the radio button one should be similar.


final JCheckBoxMenuItem cbItem = new JCheckBoxMenuItem("Check box 1");

cbItem.getModel().addChangeListener(new ChangeListener() {
    @Override
    public void stateChanged(ChangeEvent e) {
        if (mbBalanceSet.getModel().isArmed() && mbBalanceSet.isShowing())
            pathToCheckBoxItem = MenuSelectionManager.defaultManager().getSelectedPath();
        }
    });
cbItem.addActionListener(new ActionListener() {
    @Override
	public void actionPerformed(ActionEvent e) {
		// destired actions
        MenuSelectionManager.defaultManager().setSelectedPath(pathToBalanceSetCheckBox);
    }
});

JMenu level1 = new JMenu("Level 1");
JMenu level2 = new JMenu("Level 2);

level2.add(cbItem);
level1.add(level2);

For the user, the reopening is not visible at all and would be fine for my solution. Is there some API element in DockingFrames I could use to get (and afterwards set) the selected menu path (similar to the MenuSelectionManager)? If it is not directly accessible with the current API version I could also overwrite and extend some existing parts, I just could not find where I should start :wink:

Thanks alot!
Christian

I played around a bit with your solution. Before I post some results, I would like to introduce you to the „CPanelPopup“. It’s basically a dialog that behaves like a menu, and that does not close so easily. It’s look and feel however is not like a menu. Try this out:


import java.awt.Color;
import java.awt.GridLayout;

import javax.swing.BorderFactory;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JPanel;

import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.DefaultSingleCDockable;
import bibliothek.gui.dock.common.action.CPanelPopup;

public class OpenMenuTest2 {
	public static void main( String[] args ) {
		JFrame frame = new JFrame();
		frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
		frame.setBounds( 50, 50, 800, 500 );
		
		CControl control = new CControl( frame );
		frame.add( control.getContentArea() );
		
		CPanelPopup popup = new CPanelPopup();
		popup.setText( "P" );
		popup.setShowTextOnButtons( true );
		
		JPanel panel = new JPanel( new GridLayout( 3, 1 ));
		popup.setContent( panel );
		panel.setBorder( BorderFactory.createLineBorder( Color.BLACK ) );
		panel.add( new JCheckBox( "Check Box A" ) );
		panel.add( new JCheckBox( "Check Box B" ) );
		panel.add( new JCheckBox( "Check Box C" ) );
		
		DefaultSingleCDockable dockable = new DefaultSingleCDockable( "id", "Title" );
		dockable.addAction( popup );
		control.addDockable( dockable );
		dockable.setVisible( true );
				
		frame.setVisible( true );
	}	
}

*** Edit ***

  1. You will need version 1.1.2p11 to run this code.
  2. It does not work. :frowning:

Well, it might work, but unfortunately the framework is not opening a JMenu, but a JPopupMenu. And JPopupMenus seem to have a different behavior than normal JMenus. I searched around a bit, but could not find any satisfying solution that would keep the popupmenu open. In the end I think the only thing I can do, is to show you where the JPopupMenu is created (so you may replace it with something else), and to show you where your code would have gone.

Let me know if you find a good solution to keep a JPopupMenu open. If such a solution exists, then I am certain that it can be combined with the framework.


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

import javax.swing.AbstractButton;
import javax.swing.JFrame;
import javax.swing.JPopupMenu;
import javax.swing.MenuElement;
import javax.swing.MenuSelectionManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import bibliothek.gui.Dockable;
import bibliothek.gui.dock.action.DockActionSource;
import bibliothek.gui.dock.action.popup.ActionPopupMenu;
import bibliothek.gui.dock.action.popup.ActionPopupMenuFactory;
import bibliothek.gui.dock.action.popup.DefaultActionPopupMenu;
import bibliothek.gui.dock.action.view.ActionViewConverter;
import bibliothek.gui.dock.action.view.ViewTarget;
import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.DefaultSingleCDockable;
import bibliothek.gui.dock.common.action.CAction;
import bibliothek.gui.dock.common.action.CCheckBox;
import bibliothek.gui.dock.common.action.CMenu;
import bibliothek.gui.dock.common.action.core.CommonSimpleCheckAction;
import bibliothek.gui.dock.themes.basic.action.menu.MenuViewItem;

public class OpenMenuTest {
	public static void main( String[] args ) {
		JFrame frame = new JFrame();
		frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
		frame.setBounds( 50, 50, 800, 500 );
		
		CControl control = new CControl( frame );
		frame.add( control.getContentArea() );
		
		CMenu menu = new CMenu( "X", null );
		menu.setShowTextOnButtons( true );
		menu.add( new NotClosingCheckBox( "Top level checkbox ") );
		
		CMenu submenu = new CMenu( "Submenu", null );
		submenu.add( new NotClosingCheckBox( "Sub level" ) );
		menu.add( submenu );
		
		DefaultSingleCDockable dockable = new DefaultSingleCDockable( "id", "Title" );
		dockable.addAction( menu );
		control.addDockable( dockable );
		dockable.setVisible( true );
		
		// by setting the popup menu factory, you can replace the default JPopupMenu by any custom implementation you would like to use
		control.getController().setPopupMenuFactory( new ActionPopupMenuFactory() {
			@Override
			public ActionPopupMenu createMenu( Component owner, Dockable dockable, DockActionSource actions, Object source ) {
				JPopupMenu menu = new JPopupMenu(); // ... for example you could subclass this JPopupMenu, use a JDialog instead, etc.
				return new DefaultActionPopupMenu( dockable, actions, menu );
			}
		} );
		
		frame.setVisible( true );
	}
	
	// This section shows how you can access all the JMenuItems, and add listeners to them. 
	private static class NotClosingCheckBox extends CCheckBox{
		public NotClosingCheckBox( String text ){
			super( null );
			// The CCheckBox makes use of a CommonSimpleCheckAction to create and handle menus (CCheckBox implores the facade pattern)
			init( new NotClosingCheckBoxImpl( this ) );
			setText( text );
		}
		
		@Override
		protected void changed() {
			System.out.println( "checkbox changed" );	
		}
	}
	
	private static class NotClosingCheckBoxImpl extends CommonSimpleCheckAction{
		public NotClosingCheckBoxImpl( CAction action ) {
			super( action );
		}

		// Before a popup menu opens, this method is called and it creates the JComponent representing this item
		@Override
		public <V> V createView( ViewTarget<V> target, ActionViewConverter converter, Dockable dockable ) {
			V result = super.createView( target, converter, dockable );
			if( result instanceof MenuViewItem<?> ){
				// Using some magic, we find the JCheckBoxMenuItem that is going to be shown
				ensureNotClosing( (MenuViewItem<?>)result );
			}
			return result;
		}
	}
	
	private static void ensureNotClosing( MenuViewItem<?> item ){
		Object button = item.getItem();
		if( button instanceof AbstractButton ){
			ensureNotClosing( (AbstractButton)button );
		}
	}
	
	private static void ensureNotClosing( AbstractButton button ){
		// And here the code you sent me is called
	    new KeepOpen( button );
	}
	
	private static class KeepOpen implements ChangeListener, ActionListener{
		private AbstractButton button;
		private MenuElement[] pathToButton;
		
		public KeepOpen(AbstractButton button){
			this.button = button;
			button.getModel().addChangeListener( this );
			button.addActionListener( this );
		}
		
		public void stateChanged( ChangeEvent e ) {
			if (button.getModel().isArmed() && button.isShowing()){
                pathToButton = MenuSelectionManager.defaultManager().getSelectedPath();
            }
		}
	
		@Override
		public void actionPerformed( ActionEvent e ) {
			MenuSelectionManager.defaultManager().setSelectedPath( pathToButton );
		}
	}
}

Hi Beni,

thank you very much for the examples! I somehow did not notice, that the framework indeed is creating a JPopupMenu instead of a JMenu. That makes things a little bit more difficult.

I will try to start working at a solution with the code you provided in the bottom part of your reply. Maybe I can get it to work by extending the CommonSimpleCheckAction. I’ll get back to you as soon as I have some results to show.

Cheers,
Christian