/******************************************************************************* * Copyright (c) 2007-2008 SAS Institute Inc., ILOG S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * SAS Institute Inc. - initial API and implementation * ILOG S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.albireo.internal; import java.awt.Component; import java.awt.EventQueue; import java.awt.event.MouseEvent; import java.util.WeakHashMap; import javax.swing.JComponent; import org.eclipse.albireo.core.AwtEnvironment; import org.eclipse.albireo.core.SwingControl; import org.eclipse.albireo.core.ThreadingHandler; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; 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 SwtPopupHandler { // Gtk will not display popup menus that are parented to the // main shell. Instead we need to create and open a second // shell to own the popup. Note that this means the SWT menu // needs to be created by the client with the parent returned by // AwtEnvironment#getSwtPopupParent. private static final boolean CREATE_POPUP_PARENT_SHELL = Platform.isGtk (); // Gtk will not display popup menus that are made visible by an // AWT mouse click (mouse pressed event), unless we wait until // 1) the mouse has been released and // 2) the mouse event for entering the parent shell has been processed // This flag enables the delay // TODO: this flag effectively changes the popup trigger from mouse down to mouse up (can it be avoided?) private static final boolean DELAY_MOUSE_DOWN_SWT_POPUP_TRIGGERS = Platform.isGtk (); // TODO: in some relatively rare cases, on GTK, SWT popups are not dismissed // It can happen if you try to dismiss it with a left click while // rapidly moving the mouse. A second click will dismiss it. // TODO: SWT popups cannot be attached to JLabels. // Do we need to use ILOG's findComponentAt() method? // TODO: On windows, the AWT/Swing cursor is retained after showing the SWT popup // Setting CREATE_POPUP_PARENT_SHELL to true might be a solution, but we also // need general cursor support which might also solve this problem. private boolean pendingSwtPopup = false; // Listen to AWT mouse events and show SWT popups as necessary private final java.awt.event.AWTEventListener popupEventListener = new java.awt.event.AWTEventListener () { public void eventDispatched ( final java.awt.AWTEvent event ) { if ( event instanceof MouseEvent ) { final MouseEvent me = (MouseEvent)event; switch ( me.getID () ) { case java.awt.event.MouseEvent.MOUSE_PRESSED: boolean isTrigger = me.isPopupTrigger (); if ( DELAY_MOUSE_DOWN_SWT_POPUP_TRIGGERS && CREATE_POPUP_PARENT_SHELL && isTrigger ) { // We must delay the Swt popup here. Otherwise the parent shell will not be // properly opened if the user clicks, then drags, then releases the mouse. // System.err.println("delaying any SWT popup display"); SwtPopupHandler.this.pendingSwtPopup = true; isTrigger = false; } if ( isTrigger ) { handlePopupTrigger ( me ); } break; case java.awt.event.MouseEvent.MOUSE_RELEASED: // TODO: can both conditions below ever be true on the same event? if ( SwtPopupHandler.this.pendingSwtPopup ) { // Now handle any previously delayed popups // if (pendingSwtPopup) { // System.err.println("handling any delayed SWT popup display"); // } handlePopupTrigger ( me ); SwtPopupHandler.this.pendingSwtPopup = false; } if ( me.isPopupTrigger () ) { handlePopupTrigger ( me ); } break; case java.awt.event.MouseEvent.MOUSE_CLICKED: // TODO: is MOUSE_CLICKED a valid trigger point? if ( me.isPopupTrigger () ) { handlePopupTrigger ( me ); } break; } } } }; // The set of toolkits to which the popupEventListener has already been added. private final WeakHashMap /* java.awt.Toolkit -> Boolean */popupSupportedToolkits = new WeakHashMap (); public void monitorAwtComponent ( final Component component ) { assert EventQueue.isDispatchThread (); // Ensure the toolkit has the popupEventListener attached. final java.awt.Toolkit toolkit = component.getToolkit (); synchronized ( this.popupSupportedToolkits ) { if ( this.popupSupportedToolkits.get ( toolkit ) == null ) { toolkit.addAWTEventListener ( this.popupEventListener, java.awt.AWTEvent.MOUSE_EVENT_MASK ); this.popupSupportedToolkits.put ( toolkit, Boolean.TRUE ); } } } protected SwingControl getSwtParent ( final Component c ) { // Return the SwingControl, if any, associated with the component if ( c instanceof JComponent ) { final JComponent jc = (JComponent)c; return (SwingControl)jc.getClientProperty ( SwingControl.SWT_PARENT_PROPERTY_KEY ); } else { return null; } } protected void handlePopupTrigger ( final MouseEvent event ) { assert EventQueue.isDispatchThread (); final java.awt.Component component = (java.awt.Component)event.getSource (); int x = event.getX (); int y = event.getY (); // Climb up until the we find the SwingControl mapped to one of our parents java.awt.Component parent = component; while ( parent != null && getSwtParent ( parent ) == null ) { x += parent.getX (); y += parent.getY (); parent = parent.getParent (); } if ( parent != null ) { final SwingControl swtParent = getSwtParent ( parent ); final int xAbsolute = x; final int yAbsolute = y; final java.awt.Component subcomp = (Component)event.getSource (); showPopupMenu ( swtParent, subcomp, x, y, xAbsolute, yAbsolute ); } } // Trigger the display of the popup menu. protected void showPopupMenu ( final SwingControl swtParent, final java.awt.Component component, final int x, final int y, final int xAbsolute, final int yAbsolute ) { assert EventQueue.isDispatchThread (); Display display; try { display = swtParent.getDisplay (); } catch ( final SWTException e ) { if ( e.code == SWT.ERROR_WIDGET_DISPOSED ) { return; } else { throw e; } } final Runnable task = new Runnable () { public void run () { try { if ( !swtParent.isDisposed () ) { showPopupMenuSWTThread ( swtParent, component, x, y, xAbsolute, yAbsolute ); } } catch ( final Throwable e ) { e.printStackTrace (); } } }; try { ThreadingHandler.getInstance ().asyncExec ( display, task ); } catch ( final SWTException e ) { if ( e.code == SWT.ERROR_DEVICE_DISPOSED ) { return; } else { throw e; } } } // Trigger the display of the popup menu from the SWT thread. protected void showPopupMenuSWTThread ( final SwingControl swtParent, final java.awt.Component component, final int x, final int y, final int xAbsolute, final int yAbsolute ) { assert Display.getCurrent () != null; final Menu menu = swtParent.getMenu ( component, x, y, xAbsolute, yAbsolute ); final Display display = swtParent.getDisplay (); if ( menu != null ) { final Shell popupParent = AwtEnvironment.getInstance ( display ).getSwtPopupParent ( swtParent ); if ( DELAY_MOUSE_DOWN_SWT_POPUP_TRIGGERS ) { // Install a listener that waits until the popup parent receives // a MouseEnter event before displaying the menu. If we don't wait // for this event, the menu is not displayed. popupParent.addListener ( SWT.MouseEnter, new Listener () { public void handleEvent ( final Event e ) { if ( CREATE_POPUP_PARENT_SHELL ) { // Install a listener to hide the (created) popup parent once the menu is // dismissed. (Don't count on 0x0 sized shells to be invisible without this) menu.addListener ( SWT.Hide, new Listener () { public void handleEvent ( final Event e ) { if ( !popupParent.isDisposed () ) { // System.err.println("hiding popup parent"); popupParent.setVisible ( false ); } // Clean up menu.removeListener ( SWT.Hide, this ); } } ); } if ( !menu.isDisposed () ) { menu.setVisible ( true ); } // Clean up popupParent.removeListener ( SWT.MouseEnter, this ); } } ); } if ( CREATE_POPUP_PARENT_SHELL ) { // Display the created parent shell at the current cursor location // System.err.println("*** opening popup parent" + display.getCursorLocation()); popupParent.setLocation ( display.getCursorLocation () ); popupParent.open (); // The menu is made visible in the listener above } if ( !DELAY_MOUSE_DOWN_SWT_POPUP_TRIGGERS ) { // If we are not waiting for MouseEnter, then open the menu right here menu.setVisible ( true ); } } } // ======================================================================== // Singleton design pattern private static SwtPopupHandler theHandler = new SwtPopupHandler (); /** * Returns the currently active singleton of this class. */ public static SwtPopupHandler getInstance () { return theHandler; } /** * Replaces the singleton of this class. * @param instance An instance of this class or of a customized subclass. */ public static void setInstance ( final SwtPopupHandler instance ) { theHandler = instance; } }