4

I have an Eclipse RCP / SWT application with a Menu of Check Box Menu Items.

I would like to be able to check/uncheck multiple items before clicking elsewhere to close the menu. However, the default SWT behavior is to close the menu after a single click.

I have implemented the following very hacked solution which works, but is certainly not elegant and probably won't work properly on all platforms or under all circumstances. So I'm very interested in a simpler technique if one exists.

The following code should compile and run inside eclipse right out of the box (apologies for the length, its the shortest self contained example I could create):

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuListener2;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Shell;

public class MenuTest
{
    public static void main( String[] args )
    {
        // create a SWT Display and Shell
        final Display display = new Display( );
        Shell shell = new Shell( display );
        shell.setText( "Menu Example" );

        // create a jface MenuManager and Menu
        MenuManager popupMenu = new MenuManager( );
        Menu menu = popupMenu.createContextMenu( shell );
        shell.setMenu( menu );

        // create a custom listener class
        final PopupListener listener = new PopupListener( shell, menu );

        // attach the listener to the Manager, Menu, and Shell (yuck!)
        popupMenu.addMenuListener( listener );
        menu.addListener( SWT.Show, listener );
        shell.addListener( SWT.MouseDown, listener );

        // add an item to the menu
        popupMenu.add( new Action( "Test", Action.AS_CHECK_BOX )
        {
            @Override
            public void run( )
            {
                System.out.println( "Test checked: " + isChecked( ) );

                listener.keepMenuVisible( );
            }
        } );

        // show the SWT shell
        shell.setSize( 800, 800 );
        shell.setLocation( 0, 0 );
        shell.open( );
        shell.moveAbove( null );

        while ( !shell.isDisposed( ) )
            if ( !display.readAndDispatch( ) ) display.sleep( );

        return;
    }

    public static class PopupListener implements Listener, IMenuListener2 
    {
        Menu menu;
        Control control;
        Point point;

        public PopupListener( Control control, Menu menu )
        {
            this.control = control;
            this.menu = menu;
        }

        @Override
        public void handleEvent( Event event )
        {
            // when SWT.Show events are received, make the Menu visible
            // (we'll programmatically create such events)
            if ( event.type == SWT.Show )
            {
                menu.setVisible( true );
            }
            // when the mouse is clicked, map the position from Shell
            // coordinates to Display coordinates and save the result
            // this is necessary because there appears to be no way
            // to ask the Menu what its current position is
            else if ( event.type == SWT.MouseDown )
            {   
                point = Display.getDefault( ).map( control, null, event.x, event.y );
            }
        }

        @Override
        public void menuAboutToShow( IMenuManager manager )
        {
            // if we have a saved point, use it to set the menu location
            if ( point != null )
            {
                menu.setLocation( point.x, point.y );
            }
        }

        @Override
        public void menuAboutToHide( IMenuManager manager )
        {
            // do nothing
        }

        // whenever the checkbox action is pressed, the menu closes
        // we run this to reopen the menu
        public void keepMenuVisible( )
        {
            Display.getDefault( ).asyncExec( new Runnable( )
            {
                @Override
                public void run( )
                {
                    Event event = new Event( );
                    event.type = SWT.Show;
                    event.button = 3;

                    menu.notifyListeners( SWT.Show, event );
                    if ( point != null )
                    {
                        menu.setLocation( point.x, point.y );
                    }
                }
            } );
        }
    }
}
ulmangt
  • 5,343
  • 3
  • 23
  • 36

1 Answers1

4

I tried your code on Win7 32bit and with eclipse 4.2. Unfortunately it was giving problem and was flickering. Anyway, here is another variation. In my opinion you have to use at least two listeners, one for your menu item(s), which in any case are needed, and the other to get the co-ordinates of the menu:

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MenuDetectEvent;
import org.eclipse.swt.events.MenuDetectListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;

public class TestMenu 
{
    private static Point point;

    public static void main(String[] args) 
    {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setLayout(new GridLayout(1, false));

        final Menu menu = new Menu(shell);
        MenuItem item = new MenuItem(menu, SWT.CHECK);
        item.setText("Check 1");
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent e) 
            {
                if(point == null) 
                    return;
                menu.setLocation(point);
                menu.setVisible(true);
            }
        });

        shell.addMenuDetectListener(new MenuDetectListener() {
            public void menuDetected(MenuDetectEvent e) {
                point = new Point(e.x, e.y);
            }
        });

        shell.setMenu(menu);

        shell.open();
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch())
                display.sleep();
        }
        display.dispose();
    }
}


Update

Thanks to @Baz, please see the comment below.

Tried this on Linux 32bit with eclipse 3.6.2 and unfortunately it doesn't seem to work.

Update 2 (by ulmangt)

Below is a modification of your solution which works for me on both 64-bit Windows 7 and 64-bit Ubuntu Linux.

The org.eclipse.swt.widgets.Menu class has a package protected field which determines whether the menu location has been set. If not, on Linux at least, the menu appears under the mouse click.

So getting the right behavior requires using reflection to reset this boolean field to false. Alternatively, the menu could probably be disposed and recreated.

Finally, Linux appears to like menu.setLocation( point ) and menu.setVisible( true ) to be made from an asyncExec block.

import java.lang.reflect.Field;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MenuDetectEvent;
import org.eclipse.swt.events.MenuDetectListener;
import org.eclipse.swt.events.MenuEvent;
import org.eclipse.swt.events.MenuListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;

public class MenuTest
{
    private static Point point;

    public static void main( String[] args )
    {
        Display display = new Display( );
        final Shell shell = new Shell( display );
        shell.setLayout( new GridLayout( 1, false ) );

        final Menu menu = new Menu( shell );
        MenuItem item = new MenuItem( menu, SWT.CHECK );
        item.setText( "Check 1" );
        item.addSelectionListener( new SelectionAdapter( )
        {
            public void widgetSelected( final SelectionEvent e )
            {
                if ( point == null ) return;

                Display.getDefault( ).asyncExec( new Runnable( )
                {
                    @Override
                    public void run( )
                    {
                        menu.setLocation( point );
                        menu.setVisible( true );
                    }
                } );
            }
        } );

        shell.addMenuDetectListener( new MenuDetectListener( )
        {
            public void menuDetected( MenuDetectEvent e )
            {
                point = new Point( e.x, e.y );
            }
        } );

        menu.addMenuListener( new MenuListener( )
        {
            @Override
            public void menuHidden( MenuEvent event )
            {
                try
                {
                    Field field = Menu.class.getDeclaredField( "hasLocation" );
                    field.setAccessible( true );
                    field.set( menu, false );
                }
                catch ( Exception e )
                {
                    e.printStackTrace();
                }
            }

            @Override
            public void menuShown( MenuEvent event )
            {
            }
        });

        shell.setMenu( menu );

        shell.open( );
        while ( !shell.isDisposed( ) )
        {
            if ( !display.readAndDispatch( ) ) display.sleep( );
        }
        display.dispose( );
    }
}
ulmangt
  • 5,343
  • 3
  • 23
  • 36
Favonius
  • 13,959
  • 3
  • 55
  • 95
  • Tried this on Linux 32bit with eclipse 3.6.2 and unfortunately it doesn't seem to work. The menu hides after clicking and on the next right-click (on different location) it appears on the previous location. – Baz Sep 06 '12 at 07:43
  • @Baz - Seems like OS dependent feature. Updating my answer. – Favonius Sep 06 '12 at 07:45
  • @Baz - Have you tried the original snippet provided by the ulmangt. Is it working under 32Bit Linux? – Favonius Sep 06 '12 at 07:49
  • Yes, that did work. The only thing that could be improved is the fact, that the Action/MenuItem is not highlighted after the menu is reopened. I have to move the mouse in order to achieve this. However, there _should_ be an easier way to solve this. Tried it myself but didn't succeed. – Baz Sep 06 '12 at 07:52
  • @Favonius I believe with a few tweaks to your solution, I have something which is working in both Linux and Windows. It's quite a hack though, as it requires reflection. Anyway, I edited your answer with my updated code. – ulmangt Sep 06 '12 at 16:17
  • @ulmangt - great :) . though one question, why are you using `Display.getDefault( ).asyncExec()` within the listener ? – Favonius Sep 06 '12 at 17:21
  • @ulmangt Haven't looked in detail at your code, but it seems to behave exactly the same as the code of the OP. – Baz Sep 06 '12 at 18:08
  • @Favonius on 64-bit Linux, Eclipse 3.7.1, the menu disappears after the selection is made without the `Display.getDefault( ).asyncExec()` call. I know it looks strange (because we're already on the event handling thread) but for whatever reason it makes it work. – ulmangt Sep 06 '12 at 18:20
  • @Baz You're correct. However I think it's more portable to different versions of Eclipse and different platforms. The original solution doesn't work for me on Windows 7, Eclipse 4.2.0. However my edit works both on Windows 7, Eclipse 4.2.0 and 64-bit Linux, Eclipse 3.7.1 (which are the only two configurations which I have tested). – ulmangt Sep 06 '12 at 18:24
  • I have a similar requirement. I need to apply filters for an expensive query only after the user is finished selecting filter options. Unfortunately with this method it's hard to detect when the menu is REALLY closed. The menuHidden event is always fired BEFORE the check item selection event that reopens the Menu, so in the menuHidden event we cannot decide whether it's a real close. The alternative I will use is a popup Window that contains a custom composite with SWT.Check buttons instead of menu items. – Henno Vermeulen Dec 03 '13 at 12:06