Setting an Accelerator on a CMenu

Hello all! Fairly new to DockingFrames, using version 1.1.3p1.

I’ve got a MultipleDockable instance to which I’ve added a simple CMenu:

    MultipleDockable parentDockable ;
    CMenu testMenu = new CMenu() ;
    {
        testMenu.setText( "L\u0332ist" );
        testMenu.setShowTextOnButtons( true );
        testMenu.setTooltip( "Show the List [ALT-L]" );
        testMenu.setAccelerator( KeyStroke.getKeyStroke( KeyEvent.VK_L, InputEvent.ALT_DOWN_MASK ) );
        //testMenu.setAcceleratorIsGlobal( true );

        testMenu.add( new CButton( "ACTION 1", null ) );
        testMenu.add( new CButton( "ACTION 2", null ) );
    }
    parentDockable.addAction( testMenu ) ;

My intent is to trigger the CMenu via ALT-L (when the dockable has focus, of course). However, the above code executes fine but ALT-L doesn’t do anything. Setting the accelerator as Global doesn’t seem to matter either. Is there more setup I need to do to get an Accelerator to work with a CMenu? I’ve tried other keys as well with no luck.

Alternately, I could set up a keyboard listener on parentDockable, catch the ALT-L keyPressed event, and open the menu. Tried that, but I can’t find docs on how to „trigger“ the CMenu to open from the keyboard listener.

Thoughts/suggestions would be greatly appreciated. Thanks!

1 „Gefällt mir“

The menus do not support accelerators: one instance of a CMenu (or any CAction) can be shown in many places, but the CAction object does not have a reference to its (many) representations as UI element.

Or in other words: a CMenu does not know where it is shown, and thus cannot access parts of the UI. If it would know the UI, it would still not know which menu exactly to open.

Other elements like a „button“ do not have this limitation, because such actions can be triggered without giving the user any visual feedback.


I was playing around a bit and added a KeyListener that will open a menu. This solution should work decently as long as you don’t have more than one JComponent that is linked to the same CMenu. Check out the example below, and let me know if it is of any help to you.

You should not run into these kinds of troubles, assuming you don’t write heavy modifications (e.g. like adding additional custom DockTitles).

package test;

import java.awt.Color;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;

import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.KeyStroke;

import bibliothek.gui.DockController;
import bibliothek.gui.Dockable;
import bibliothek.gui.dock.action.ActionType;
import bibliothek.gui.dock.action.DockAction;
import bibliothek.gui.dock.action.MenuDockAction;
import bibliothek.gui.dock.action.view.ActionViewConverter;
import bibliothek.gui.dock.action.view.ViewGenerator;
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.CButton;
import bibliothek.gui.dock.common.action.CMenu;
import bibliothek.gui.dock.common.action.core.CommonDockAction;
import bibliothek.gui.dock.common.event.CKeyboardListener;
import bibliothek.gui.dock.common.intern.CDockController;
import bibliothek.gui.dock.common.intern.CDockable;
import bibliothek.gui.dock.common.intern.CommonDockable;
import bibliothek.gui.dock.common.intern.EfficientControlFactory;
import bibliothek.gui.dock.control.ControllerSetupCollection;
import bibliothek.gui.dock.control.DefaultDockControllerFactory;
import bibliothek.gui.dock.themes.basic.action.BasicMenuHandler;
import bibliothek.gui.dock.themes.basic.action.BasicTitleViewItem;
import bibliothek.gui.dock.title.DockTitle.Orientation;

public class AcceleratorTest {
	public static void main( String[] args ) {
		JFrame frame = new JFrame();
		frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
		frame.setBounds( 50, 50, 1000, 1000 );

		// note the custom factory we are creating
		CControl control = new CControl( frame, new CustomCControlFactory() );

		// control.setTheme( ThemeMap.KEY_ECLIPSE_THEME );
		frame.add( control.getContentArea() );

		DefaultSingleCDockable dockable = new DefaultSingleCDockable( "id", "Title" );
		dockable.add( new JButton( "Focus me" ) );
		setup( dockable );
		control.addDockable( dockable );
		dockable.setVisible( true );

		frame.setVisible( true );
	}

	/*
	 * Adding the menu
	 */
	private static void setup( DefaultSingleCDockable parentDockable ) {
		CMenu testMenu = new CMenu();

		testMenu.setText( "L\u0332ist" );
		testMenu.setShowTextOnButtons( true );
		testMenu.setTooltip( "Show the List [ALT-L]" );
		testMenu.setAccelerator( KeyStroke.getKeyStroke( KeyEvent.VK_L, InputEvent.ALT_DOWN_MASK ) );
		// testMenu.setAcceleratorIsGlobal( true );

		testMenu.add( new CButton( "ACTION 1", null ) );
		testMenu.add( new CButton( "ACTION 2", null ) );

		parentDockable.addAction( testMenu );
	}

	/*
	 * This factory creates a special CustomControllerFactory ...
	 */
	private static class CustomCControlFactory extends EfficientControlFactory {
		@Override
		public DockController createController( CControl owner ) {
			return new CDockController( owner, new CustomControllerFactory() );
		}
	}

	/*
	 * ... the CustomControllerFactory creates a special CustomActionViewConverter ...
	 */
	private static class CustomControllerFactory extends DefaultDockControllerFactory {
		@Override
		public ActionViewConverter createActionViewConverter( DockController controller, ControllerSetupCollection setup ) {
			return new CustomActionViewConverter();
		}
	}

	/*
	 * ... and the CustomActionViewConverter is responsible for creating JComponents for buttons, menus, etc.
	 */
	private static class CustomActionViewConverter extends ActionViewConverter {
		@Override
		public <A, D extends DockAction> void putClient( ActionType<D> action, ViewTarget<A> target, ViewGenerator<D, A> generator ) {
			generator = replace( action, target, generator );
			super.putClient( action, target, generator );
		}

		@Override
		public <A, D extends DockAction> void putTheme( ActionType<D> action, ViewTarget<A> target, ViewGenerator<D, A> generator ) {
			generator = replace( action, target, generator );
			super.putTheme( action, target, generator );
		}

		@Override
		public <A, D extends DockAction> void putDefault( ActionType<D> action, ViewTarget<A> target, ViewGenerator<D, A> generator ) {
			generator = replace( action, target, generator );
			super.putDefault( action, target, generator );
		}

		@SuppressWarnings("unchecked")
		private <A, D extends DockAction> ViewGenerator<D, A> replace( ActionType<D> action, ViewTarget<A> target, ViewGenerator<D, A> generator ) {
			if( action == ActionType.MENU && target == ViewTarget.TITLE ) {
				/*
				 * If where are showing a menu in a title: wrap the "handler" in a special CustomMenuViewGenerator ...
				 */
				return (ViewGenerator<D, A>) replace( (ViewGenerator<MenuDockAction, BasicTitleViewItem<JComponent>>) generator );
			} else {
				return generator;
			}
		}

		private ViewGenerator<MenuDockAction, BasicTitleViewItem<JComponent>> replace( ViewGenerator<MenuDockAction, BasicTitleViewItem<JComponent>> generator ) {
			return new CustomMenuViewGenerator( generator );
		}
	}

	/*
	 * ... the CustomMenuViewGenerator creates a special CustomBasicTitleViewItem when we are dealing with a CMenu on a
	 * CDockable ...
	 */
	private static class CustomMenuViewGenerator implements ViewGenerator<MenuDockAction, BasicTitleViewItem<JComponent>> {
		private final ViewGenerator<MenuDockAction, BasicTitleViewItem<JComponent>> generator;

		public CustomMenuViewGenerator( ViewGenerator<MenuDockAction, BasicTitleViewItem<JComponent>> generator ) {
			this.generator = generator;
		}

		@Override
		public BasicTitleViewItem<JComponent> create( ActionViewConverter converter, MenuDockAction action, Dockable dockable ) {
			BasicTitleViewItem<JComponent> delegate = generator.create( converter, action, dockable );
			CMenu menu = getMenu( action );

			if( dockable instanceof CommonDockable && delegate instanceof BasicMenuHandler && menu != null ) {
				return new CustomBasicTitleViewItem( menu, ((CommonDockable) dockable).getDockable(), (BasicMenuHandler) delegate );
			} else {
				return delegate;
			}
		}

		private CMenu getMenu( MenuDockAction action ) {
			if( action instanceof CommonDockAction ) {
				CAction caction = ((CommonDockAction) action).getAction();
				if( caction instanceof CMenu ) {
					return (CMenu) caction;
				}
			}
			return null;
		}
	}

	/*
	 * ... finally the CustomBasicTitleViewItem. Here we add a KeyListener to the CDockable and open the menu when the
	 * accelerator of the menu is hit.
	 */
	private static class CustomBasicTitleViewItem implements BasicTitleViewItem<JComponent> {
		private final BasicTitleViewItem<JComponent> delegate;
		private final CDockable dockable;
		private final MenuOpener menuOpener;

		public CustomBasicTitleViewItem( CMenu menu, CDockable dockable, BasicMenuHandler delegate ) {
			this.delegate = delegate;
			this.dockable = dockable;
			this.menuOpener = new MenuOpener( menu, delegate );
		}

		@Override
		public void bind() {
			delegate.bind();
			/*
			 * Register the KeyListener
			 */
			dockable.addKeyboardListener( menuOpener );
		}

		@Override
		public void unbind() {
			/*
			 * Clean up the KeyListener
			 */
			dockable.removeKeyboardListener( menuOpener );
			delegate.unbind();
		}

		@Override
		public JComponent getItem() {
			return delegate.getItem();
		}

		@Override
		public DockAction getAction() {
			return delegate.getAction();
		}

		@Override
		public void setOrientation( Orientation orientation ) {
			delegate.setOrientation( orientation );
		}

		@Override
		public void setForeground( Color foreground ) {
			delegate.setForeground( foreground );
		}

		@Override
		public void setBackground( Color background ) {
			delegate.setBackground( background );
		}
	}

	private static class MenuOpener implements CKeyboardListener {
		private final CMenu menu;
		private final BasicMenuHandler delegate;

		public MenuOpener( CMenu menu, BasicMenuHandler delegate ) {
			this.menu = menu;
			this.delegate = delegate;
		}

		private void open() {
			delegate.triggered();
		}

		@Override
		public boolean keyTyped( CDockable source, KeyEvent event ) {
			return false;
		}

		@Override
		public boolean keyReleased( CDockable source, KeyEvent event ) {
			System.out.println( event );

			/*
			 * open the menu if its accelerator is hit
			 */
			KeyStroke accelerator = menu.getAccelerator();
			if( accelerator != null && accelerator.equals( KeyStroke.getKeyStrokeForEvent( event ) ) ) {
				open();
				return true;
			}
			return false;
		}

		@Override
		public boolean keyPressed( CDockable source, KeyEvent event ) {
			System.out.println( event );

			/*
			 * open the menu if its accelerator is hit
			 */
			KeyStroke accelerator = menu.getAccelerator();
			if( accelerator != null && accelerator.equals( KeyStroke.getKeyStrokeForEvent( event ) ) ) {
				open();
				return true;
			}
			return false;
		}
	}
}

Thanks for the input and clarifications. I haven’t had a chance to try the KeyListener example yet but I’ll come back and update as soon as I get a chance.