/******************************************************************************* * Copyright (c) 2007, 2013 Association for Decentralized Information Management * in Industry THTH ry. * 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: * VTT Technical Research Centre of Finland - initial API and implementation * Semantum Oy - workaround for Simantics issue #3518 *******************************************************************************/ package org.simantics.utils.ui; import java.awt.AWTEvent; import java.awt.Component; import java.awt.Container; import java.awt.EventQueue; import java.awt.Frame; import java.awt.GridLayout; import java.awt.Toolkit; import java.awt.event.AWTEventListener; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import javax.swing.JApplet; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.plaf.FontUIResource; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.swt.SWT; import org.eclipse.swt.awt.SWT_AWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; 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.Shell; import org.simantics.utils.threads.AWTThread; import org.simantics.utils.threads.ThreadUtils; import org.simantics.utils.threads.logger.ITask; import org.simantics.utils.threads.logger.ThreadLogger; import org.simantics.utils.ui.internal.Activator; import org.simantics.utils.ui.internal.awt.AwtEnvironment; import org.simantics.utils.ui.internal.awt.AwtFocusHandler; import org.simantics.utils.ui.internal.awt.CleanResizeListener; import org.simantics.utils.ui.internal.awt.EmbeddedChildFocusTraversalPolicy; import org.simantics.utils.ui.internal.awt.SwtFocusHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** *
 *        embeddedComposite = new SWTAWTComposite(parent, SWT.NONE) {
 *            protected JComponent createSwingComponent() {
 *                scrollPane = new JScrollPane();
 *                table = new JTable();
 *                scrollPane.setViewportView(table);
 *                return scrollPane;
 *            }
 *        };
 *        // For asynchronous AWT UI population of the swing components:
 *        embeddedComposite.populate();
 *        // and optionally you can wait until the AWT UI population
 *        // has finished:
 *        embeddedComposite.waitUntilPopulated();
 *
 *        // OR:
 *
 *        // Do both things above in one call to block until the
 *        // AWT UI population is complete:
 *        embeddedComposite.syncPopulate();
 *
 *        // OR:
 *
 *        // Set a callback for asynchronous completion in the AWT thread:
 *        embeddedComposite.populate(component -> {
              // AWT components have been created for component
 *        });
 *
 *        // All methods assume all invocations are made from the SWT display thread.
 * 
*

* * @author Tuukka Lehtonen */ public abstract class SWTAWTComponent extends Composite { private static final Logger LOGGER = LoggerFactory.getLogger(SWTAWTComponent.class); private static class AwtContext { private Frame frame; private Component swingComponent; AwtContext(Frame frame) { assert frame != null; this.frame = frame; } Frame getFrame() { return frame; } void setSwingComponent(Component swingComponent) { this.swingComponent = swingComponent; } Component getSwingComponent() { return swingComponent; } } private Font currentSystemFont; private AwtContext awtContext; private AwtFocusHandler awtHandler; private JApplet panel; private final AtomicBoolean populationStarted = new AtomicBoolean(false); private final AtomicBoolean populated = new AtomicBoolean(false); private final Semaphore populationSemaphore = new Semaphore(0); private Consumer populatedCallback; private static AWTEventListener awtListener = null; private Listener settingsListener = new Listener() { public void handleEvent(Event event) { handleSettingsChange(); } }; // This listener helps ensure that Swing popup menus are properly dismissed when // a menu item off the SWT main menu bar is shown. private final Listener menuListener = new Listener() { public void handleEvent(Event event) { assert awtHandler != null; awtHandler.postHidePopups(); } }; public SWTAWTComponent(Composite parent, int style) { super(parent, style | SWT.NO_BACKGROUND | SWT.NO_REDRAW_RESIZE | SWT.EMBEDDED); getDisplay().addListener(SWT.Settings, settingsListener); setLayout(new FillLayout()); currentSystemFont = getFont(); this.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { doDispose(); } }); } protected void doDispose() { getDisplay().removeListener(SWT.Settings, settingsListener); getDisplay().removeFilter(SWT.Show, menuListener); ThreadUtils.asyncExec(AWTThread.getThreadAccess(), new Runnable() { @Override public void run() { AwtContext ctx = awtContext; if (ctx != null) { ctx.frame.dispose(); } awtContext = null; if (panel != null) { panel.removeAll(); panel = null; } } }); } static class FocusRepairListener implements AWTEventListener { @Override public void eventDispatched(AWTEvent e) { if (e.getID() == MouseEvent.MOUSE_PRESSED) { Object src = e.getSource(); if (src instanceof Component) { ((Component) src).requestFocus(); } } } } /** * Create a global AWTEventListener for focus management. * This helps at least with Linux/GTK problems of transferring focus * to workbench parts when clicking on AWT screen territory. * * NOTE: There is really no need to dispose this once it's been initialized. * * NOTE: must be invoked from AWT thread. */ private static synchronized void initAWTEventListener() { if (!AWTThread.getThreadAccess().currentThreadAccess()) throw new AssertionError("not invoked from AWT thread"); if (awtListener == null) { awtListener = new FocusRepairListener(); Toolkit.getDefaultToolkit().addAWTEventListener(awtListener, AWTEvent.MOUSE_EVENT_MASK); } } protected Container getContainer() { return panel; } public Component getAWTComponent() { assert awtContext != null; return awtContext.getSwingComponent(); } /** * This method must always be called from SWT thread. This method should be * used with extreme care since it will block the calling thread (i.e. the * SWT thread) while the AWT thread initializes itself by spinning and * dispatching SWT events. This diminishes the possibility of deadlock * (reported between AWT and SWT) but still all UI's are recommended to use * the asynchronous non-blocking UI population offered by * {@link #populate(Consumer)} * * @see #populate(Consumer) */ public void syncPopulate() { populate(); waitUntilPopulated(); } /** * This method must always be called from SWT thread. This will schedule the * real AWT component creation into the AWT thread and call the provided * asynchronous callback after the UI population is complete. * This prevents the possibility of deadlocking. */ public void populate(Consumer callback) { populate(); this.populatedCallback = callback; } /** * This method will create an AWT {@link Frame} through {@link SWT_AWT} and * schedule AWT canvas initialization into the AWT thread. It will not wait * for AWT initialization to complete. */ public void populate() { if (!populationStarted.compareAndSet(false, true)) throw new IllegalStateException(this + ".populate was invoked multiple times"); checkWidget(); ITask task = ThreadLogger.getInstance().begin("createFrame"); createFrame(); task.finish(); scheduleComponentCreation(); } public void waitUntilPopulated() { if (populated.get()) return; try { boolean done = false; while (!done) { done = populationSemaphore.tryAcquire(10, TimeUnit.MILLISECONDS); while (!done && getDisplay().readAndDispatch()) { /* * Note: readAndDispatch can cause this to be disposed. */ if(isDisposed()) return; done = populationSemaphore.tryAcquire(); } } } catch (InterruptedException e) { throw new Error("EmbeddedSwingComposite population interrupted for class " + this, e); } } /** * Returns the embedded AWT frame. The returned frame is the root of the AWT containment * hierarchy for the embedded Swing component. This method can be called from * any thread. * * @return the embedded frame */ public Frame getFrame() { // Intentionally leaving out checkWidget() call. This may need to be called from within user's // createSwingComponent() method. Accessing from a non-SWT thread is OK, but we still check // for disposal if (getDisplay() == null || isDisposed()) { SWT.error(SWT.ERROR_WIDGET_DISPOSED); } AwtContext ctx = awtContext; return (ctx != null) ? ctx.getFrame() : null; } private void createFrame() { assert Display.getCurrent() != null; // On SWT event thread // Make sure Awt environment is initialized. AwtEnvironment.getInstance(getDisplay()); if (awtContext != null) { final Frame oldFrame = awtContext.getFrame(); // Schedule disposal of old frame on AWT thread so that there are no problems with // already-scheduled operations that have not completed. // Note: the implementation of Frame.dispose() would schedule the use of the AWT // thread even if it was not done here, but it uses invokeAndWait() which is // prone to deadlock (and not necessary for this case). EventQueue.invokeLater(new Runnable() { public void run() { oldFrame.dispose(); } }); } Frame frame = SWT_AWT.new_Frame(this); awtContext = new AwtContext(frame); // See Simantics issue #3518 workaroundJava7FocusProblem(frame); // Glue the two frameworks together. Do this before anything is added to the frame // so that all necessary listeners are in place. createFocusHandlers(); // This listener clears garbage during resizing, making it looker much cleaner addControlListener(new CleanResizeListener()); } private void workaroundJava7FocusProblem(Frame frame) { String ver = System.getProperty("java.version"); String[] split = ver.split("."); if (split.length < 2) { LOGGER.warn("Focus fix listener: unrecognized Java version: " + ver); return; } try { int major = Integer.parseInt(split[0]); int minor = Integer.parseInt(split[1]); if ((major == 1 && (minor == 7 || minor == 8)) || major >= 9) { try { frame.addWindowListener(new Java7FocusFixListener(this, frame)); } catch (SecurityException e) { Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e)); } catch (NoSuchMethodException e) { Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e)); } } } catch (NumberFormatException e) { LOGGER.error("Focus fix listener: unrecognized Java version: " + ver); } } static class Java7FocusFixListener extends WindowAdapter { Method shellSetActiveControl; Control control; Frame frame; public Java7FocusFixListener(Control control, Frame frame) throws NoSuchMethodException, SecurityException { this.shellSetActiveControl = Shell.class.getDeclaredMethod("setActiveControl", Control.class); this.frame = frame; this.control = control; } @Override public void windowActivated(WindowEvent e) { SWTUtils.asyncExec(control, new Runnable() { @Override public void run() { if (control.isDisposed()) return; if (control.getDisplay().getFocusControl() == control) { try { boolean accessible = shellSetActiveControl.isAccessible(); if (!accessible) shellSetActiveControl.setAccessible(true); shellSetActiveControl.invoke(control.getShell(), control); if (!accessible) shellSetActiveControl.setAccessible(false); } catch (SecurityException e) { Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e)); } catch (IllegalArgumentException e) { Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e)); } catch (IllegalAccessException e) { Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e)); } catch (InvocationTargetException e) { Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getCause().getMessage(), e.getCause())); } } } }); } } private void createFocusHandlers() { assert awtContext != null; assert Display.getCurrent() != null; // On SWT event thread Frame frame = awtContext.getFrame(); awtHandler = new AwtFocusHandler(frame); SwtFocusHandler swtHandler = new SwtFocusHandler(this); awtHandler.setSwtHandler(swtHandler); swtHandler.setAwtHandler(awtHandler); // Ensure that AWT pop-ups are dismissed whenever a SWT menu is shown getDisplay().addFilter(SWT.Show, menuListener); EmbeddedChildFocusTraversalPolicy policy = new EmbeddedChildFocusTraversalPolicy(awtHandler); frame.setFocusTraversalPolicy(policy); } private void scheduleComponentCreation() { assert awtContext != null; // Create AWT/Swing components on the AWT thread. This is // especially necessary to avoid an AWT leak bug (6411042). final AwtContext currentContext = awtContext; ThreadUtils.asyncExec(AWTThread.getThreadAccess(), new Runnable() { @Override public void run() { // Make sure AWT focus fix is in place. initAWTEventListener(); panel = addRootPaneContainer(currentContext.getFrame()); panel.setLayout(new GridLayout(1,1,0,0)); try { Component swingComponent = createSwingComponent(); currentContext.setSwingComponent(swingComponent); panel.getRootPane().getContentPane().add(swingComponent); //panel.add(swingComponent); setComponentFont(); } finally { // Needed to support #waitUntilPopulated populated.set(true); if (populationSemaphore != null) populationSemaphore.release(); if (populatedCallback != null) { populatedCallback.accept(SWTAWTComponent.this); populatedCallback = null; } } } }); } /** * Adds a root pane container to the embedded AWT frame. Override this to provide your own * {@link javax.swing.RootPaneContainer} implementation. In most cases, it is not necessary * to override this method. *

* This method is called from the AWT event thread. *

* If you are defining your own root pane container, make sure that there is at least one * heavyweight (AWT) component in the frame's containment hierarchy; otherwise, event * processing will not work correctly. See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4982522 * for more information. * * @param frame the frame to which the root pane container is added * @return a non-null Swing component */ protected JApplet addRootPaneContainer(Frame frame) { assert EventQueue.isDispatchThread(); // On AWT event thread assert frame != null; // It is important to set up the proper top level components in the frame: // 1) For Swing to work properly, Sun documents that there must be an implementor of // javax.swing.RootPaneContainer at the top of the component hierarchy. // 2) For proper event handling there must be a heavyweight // an AWT frame must contain a heavyweight component (see // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4982522) // 3) The Swing implementation further narrows the options by expecting that the // top of the hierarchy be a JFrame, JDialog, JWindow, or JApplet. See javax.swing.PopupFactory. // All this drives the choice of JApplet for the top level Swing component. It is the // only single component that satisfies all the above. This does not imply that // we have a true applet; in particular, there is no notion of an applet lifecycle in this // context. JApplet applet = new JApplet(); // In JRE 1.4, the JApplet makes itself a focus cycle root. This // interferes with the focus handling installed on the parent frame, so // change it back to a non-root here. // TODO: consider moving the focus policy from the Frame down to the JApplet applet.setFocusCycleRoot(false); frame.add(applet); return applet; } /** * Override this to customize what kind of AWT/Swing UI is created by this * {@link SWTAWTComponent}. * * @return the AWT/Swing component created by this SWTAWT bridging control * @thread AWT */ protected abstract Component createSwingComponent(); private void setComponentFont() { assert currentSystemFont != null; assert EventQueue.isDispatchThread(); // On AWT event thread Component swingComponent = (awtContext != null) ? awtContext.getSwingComponent() : null; if ((swingComponent != null) && !currentSystemFont.getDevice().isDisposed()) { FontData fontData = currentSystemFont.getFontData()[0]; // AWT font sizes assume a 72 dpi resolution, always. The true screen resolution must be // used to convert the platform font size into an AWT point size that matches when displayed. int resolution = Toolkit.getDefaultToolkit().getScreenResolution(); int awtFontSize = (int)Math.round((double)fontData.getHeight() * resolution / 72.0); // The style constants for SWT and AWT map exactly, and since they are int constants, they should // never change. So, the SWT style is passed through as the AWT style. java.awt.Font awtFont = new java.awt.Font(fontData.getName(), fontData.getStyle(), awtFontSize); // Update the look and feel defaults to use new font. updateLookAndFeel(awtFont); // Allow subclasses to react to font change if necessary. updateAwtFont(awtFont); // Allow components to update their UI based on new font // TODO: should the update method be called on the root pane instead? Container contentPane = SwingUtilities.getRootPane(swingComponent).getContentPane(); SwingUtilities.updateComponentTreeUI(contentPane); } } private void updateLookAndFeel(java.awt.Font awtFont) { assert awtFont != null; assert EventQueue.isDispatchThread(); // On AWT event thread // The FontUIResource class marks the font as replaceable by the look and feel // implementation if font settings are later changed. FontUIResource fontResource = new FontUIResource(awtFont); // Assign the new font to the relevant L&F font properties. These are // the properties that are initially assigned to the system font // under the Windows look and feel. // TODO: It's possible that other platforms will need other assignments. // TODO: This does not handle fonts other than the "system" font. // Other fonts may change, and the Swing L&F may not be adjusting. UIManager.put("Button.font", fontResource); //$NON-NLS-1$ UIManager.put("CheckBox.font", fontResource); //$NON-NLS-1$ UIManager.put("ComboBox.font", fontResource); //$NON-NLS-1$ UIManager.put("EditorPane.font", fontResource); //$NON-NLS-1$ UIManager.put("Label.font", fontResource); //$NON-NLS-1$ UIManager.put("List.font", fontResource); //$NON-NLS-1$ UIManager.put("Panel.font", fontResource); //$NON-NLS-1$ UIManager.put("ProgressBar.font", fontResource); //$NON-NLS-1$ UIManager.put("RadioButton.font", fontResource); //$NON-NLS-1$ UIManager.put("ScrollPane.font", fontResource); //$NON-NLS-1$ UIManager.put("TabbedPane.font", fontResource); //$NON-NLS-1$ UIManager.put("Table.font", fontResource); //$NON-NLS-1$ UIManager.put("TableHeader.font", fontResource); //$NON-NLS-1$ UIManager.put("TextField.font", fontResource); //$NON-NLS-1$ UIManager.put("TextPane.font", fontResource); //$NON-NLS-1$ UIManager.put("TitledBorder.font", fontResource); //$NON-NLS-1$ UIManager.put("ToggleButton.font", fontResource); //$NON-NLS-1$ UIManager.put("TreeFont.font", fontResource); //$NON-NLS-1$ UIManager.put("ViewportFont.font", fontResource); //$NON-NLS-1$ } /** * Performs custom updates to newly set fonts. This method is called whenever a change * to the system font through the system settings (i.e. control panel) is detected. *

* This method is called from the AWT event thread. *

* In most cases it is not necessary to override this method. Normally, the implementation * of this class will automatically propogate font changes to the embedded Swing components * through Swing's Look and Feel support. However, if additional * special processing is necessary, it can be done inside this method. * * @param newFont New AWT font */ protected void updateAwtFont(java.awt.Font newFont) { } private void handleSettingsChange() { Font newFont = getDisplay().getSystemFont(); if (!newFont.equals(currentSystemFont)) { currentSystemFont = newFont; EventQueue.invokeLater(new Runnable() { public void run() { setComponentFont(); } }); } } }