-/*******************************************************************************\r
- * Copyright (c) 2007, 2013 Association for Decentralized Information Management\r
- * in Industry THTH ry.\r
- * All rights reserved. This program and the accompanying materials\r
- * are made available under the terms of the Eclipse Public License v1.0\r
- * which accompanies this distribution, and is available at\r
- * http://www.eclipse.org/legal/epl-v10.html\r
- *\r
- * Contributors:\r
- * VTT Technical Research Centre of Finland - initial API and implementation\r
- * Semantum Oy - workaround for Simantics issue #3518\r
- *******************************************************************************/\r
-package org.simantics.utils.ui;\r
-\r
-import java.awt.AWTEvent;\r
-import java.awt.Component;\r
-import java.awt.Container;\r
-import java.awt.EventQueue;\r
-import java.awt.Frame;\r
-import java.awt.GridLayout;\r
-import java.awt.Toolkit;\r
-import java.awt.event.AWTEventListener;\r
-import java.awt.event.MouseEvent;\r
-import java.awt.event.WindowAdapter;\r
-import java.awt.event.WindowEvent;\r
-import java.lang.reflect.InvocationTargetException;\r
-import java.lang.reflect.Method;\r
-import java.util.concurrent.Semaphore;\r
-import java.util.concurrent.TimeUnit;\r
-import java.util.concurrent.atomic.AtomicBoolean;\r
-import java.util.function.Consumer;\r
-\r
-import javax.swing.JApplet;\r
-import javax.swing.SwingUtilities;\r
-import javax.swing.UIManager;\r
-import javax.swing.plaf.FontUIResource;\r
-\r
-import org.eclipse.core.runtime.IStatus;\r
-import org.eclipse.core.runtime.Status;\r
-import org.eclipse.swt.SWT;\r
-import org.eclipse.swt.awt.SWT_AWT;\r
-import org.eclipse.swt.events.DisposeEvent;\r
-import org.eclipse.swt.events.DisposeListener;\r
-import org.eclipse.swt.graphics.Font;\r
-import org.eclipse.swt.graphics.FontData;\r
-import org.eclipse.swt.layout.FillLayout;\r
-import org.eclipse.swt.widgets.Composite;\r
-import org.eclipse.swt.widgets.Control;\r
-import org.eclipse.swt.widgets.Display;\r
-import org.eclipse.swt.widgets.Event;\r
-import org.eclipse.swt.widgets.Listener;\r
-import org.eclipse.swt.widgets.Shell;\r
-import org.simantics.utils.threads.AWTThread;\r
-import org.simantics.utils.threads.ThreadUtils;\r
-import org.simantics.utils.threads.logger.ITask;\r
-import org.simantics.utils.threads.logger.ThreadLogger;\r
-import org.simantics.utils.ui.internal.Activator;\r
-import org.simantics.utils.ui.internal.awt.AwtEnvironment;\r
-import org.simantics.utils.ui.internal.awt.AwtFocusHandler;\r
-import org.simantics.utils.ui.internal.awt.CleanResizeListener;\r
-import org.simantics.utils.ui.internal.awt.EmbeddedChildFocusTraversalPolicy;\r
-import org.simantics.utils.ui.internal.awt.SwtFocusHandler;\r
-\r
-\r
-/**\r
- * <pre>\r
- * embeddedComposite = new SWTAWTComposite(parent, SWT.NONE) {\r
- * protected JComponent createSwingComponent() {\r
- * scrollPane = new JScrollPane();\r
- * table = new JTable();\r
- * scrollPane.setViewportView(table);\r
- * return scrollPane;\r
- * }\r
- * };\r
- * // For asynchronous AWT UI population of the swing components:\r
- * embeddedComposite.populate();\r
- * // and optionally you can wait until the AWT UI population\r
- * // has finished:\r
- * embeddedComposite.waitUntilPopulated();\r
- *\r
- * // OR:\r
- *\r
- * // Do both things above in one call to block until the\r
- * // AWT UI population is complete:\r
- * embeddedComposite.syncPopulate();\r
- *\r
- * // OR:\r
- *\r
- * // Set a callback for asynchronous completion in the AWT thread:\r
- * embeddedComposite.populate(component -> {\r
- // AWT components have been created for component\r
- * });\r
- *\r
- * // All methods assume all invocations are made from the SWT display thread.\r
- * </pre>\r
- * <p>\r
- * \r
- * @author Tuukka Lehtonen\r
- */\r
-public abstract class SWTAWTComponent extends Composite {\r
-\r
- private static class AwtContext {\r
- private Frame frame;\r
- private Component swingComponent;\r
-\r
- AwtContext(Frame frame) {\r
- assert frame != null;\r
- this.frame = frame;\r
- }\r
-\r
- Frame getFrame() {\r
- return frame;\r
- }\r
-\r
- void setSwingComponent(Component swingComponent) {\r
- this.swingComponent = swingComponent;\r
- }\r
-\r
- Component getSwingComponent() {\r
- return swingComponent;\r
- }\r
-\r
- }\r
-\r
- private Font currentSystemFont;\r
- private AwtContext awtContext;\r
- private AwtFocusHandler awtHandler;\r
-\r
- private JApplet panel;\r
-\r
- private final AtomicBoolean populationStarted = new AtomicBoolean(false);\r
-\r
- private final AtomicBoolean populated = new AtomicBoolean(false);\r
-\r
- private final Semaphore populationSemaphore = new Semaphore(0);\r
-\r
- private Consumer<SWTAWTComponent> populatedCallback;\r
-\r
- private static AWTEventListener awtListener = null;\r
-\r
- private Listener settingsListener = new Listener() {\r
- public void handleEvent(Event event) {\r
- handleSettingsChange();\r
- }\r
- };\r
-\r
- // This listener helps ensure that Swing popup menus are properly dismissed when\r
- // a menu item off the SWT main menu bar is shown.\r
- private final Listener menuListener = new Listener() {\r
- public void handleEvent(Event event) {\r
- assert awtHandler != null;\r
- awtHandler.postHidePopups();\r
- }\r
- };\r
-\r
- public SWTAWTComponent(Composite parent, int style) {\r
- super(parent, style | SWT.NO_BACKGROUND | SWT.NO_REDRAW_RESIZE | SWT.EMBEDDED);\r
- getDisplay().addListener(SWT.Settings, settingsListener);\r
- setLayout(new FillLayout());\r
- currentSystemFont = getFont();\r
- this.addDisposeListener(new DisposeListener() {\r
- @Override\r
- public void widgetDisposed(DisposeEvent e) {\r
- doDispose();\r
- }\r
- });\r
- }\r
-\r
- protected void doDispose() {\r
- getDisplay().removeListener(SWT.Settings, settingsListener);\r
- getDisplay().removeFilter(SWT.Show, menuListener);\r
-\r
- ThreadUtils.asyncExec(AWTThread.getThreadAccess(), new Runnable() {\r
- @Override\r
- public void run() {\r
- AwtContext ctx = awtContext;\r
- if (ctx != null) {\r
- ctx.frame.dispose();\r
- }\r
- awtContext = null;\r
- if (panel != null) {\r
- panel.removeAll();\r
- panel = null;\r
- }\r
- }\r
- });\r
- }\r
-\r
- static class FocusRepairListener implements AWTEventListener {\r
- @Override\r
- public void eventDispatched(AWTEvent e) {\r
- if (e.getID() == MouseEvent.MOUSE_PRESSED) {\r
- Object src = e.getSource();\r
- if (src instanceof Component) {\r
- ((Component) src).requestFocus();\r
- }\r
- }\r
- }\r
- }\r
-\r
- /**\r
- * Create a global AWTEventListener for focus management.\r
- * This helps at least with Linux/GTK problems of transferring focus\r
- * to workbench parts when clicking on AWT screen territory.\r
- * \r
- * NOTE: There is really no need to dispose this once it's been initialized.\r
- * \r
- * NOTE: must be invoked from AWT thread.\r
- */\r
- private static synchronized void initAWTEventListener() {\r
- if (!AWTThread.getThreadAccess().currentThreadAccess())\r
- throw new AssertionError("not invoked from AWT thread");\r
- if (awtListener == null) {\r
- awtListener = new FocusRepairListener();\r
- Toolkit.getDefaultToolkit().addAWTEventListener(awtListener, AWTEvent.MOUSE_EVENT_MASK);\r
- }\r
- }\r
-\r
- protected Container getContainer() {\r
- return panel;\r
- }\r
-\r
- public Component getAWTComponent() {\r
- assert awtContext != null;\r
- return awtContext.getSwingComponent();\r
- }\r
-\r
- /**\r
- * This method must always be called from SWT thread. This method should be\r
- * used with extreme care since it will block the calling thread (i.e. the\r
- * SWT thread) while the AWT thread initializes itself by spinning and\r
- * dispatching SWT events. This diminishes the possibility of deadlock\r
- * (reported between AWT and SWT) but still all UI's are recommended to use\r
- * the asynchronous non-blocking UI population offered by\r
- * {@link #populate(Consumer)}\r
- * \r
- * @see #populate(Consumer)\r
- */\r
- public void syncPopulate() {\r
- populate();\r
- waitUntilPopulated();\r
- }\r
-\r
- /**\r
- * This method must always be called from SWT thread. This will schedule the\r
- * real AWT component creation into the AWT thread and call the provided\r
- * asynchronous callback after the UI population is complete.\r
- * This prevents the possibility of deadlocking.\r
- */\r
- public void populate(Consumer<SWTAWTComponent> callback) {\r
- populate();\r
- this.populatedCallback = callback;\r
- }\r
-\r
- /**\r
- * This method will create an AWT {@link Frame} through {@link SWT_AWT} and\r
- * schedule AWT canvas initialization into the AWT thread. It will not wait\r
- * for AWT initialization to complete.\r
- */\r
- public void populate() {\r
- if (!populationStarted.compareAndSet(false, true))\r
- throw new IllegalStateException(this + ".populate was invoked multiple times");\r
-\r
- checkWidget();\r
- ITask task = ThreadLogger.getInstance().begin("createFrame");\r
- createFrame();\r
- task.finish();\r
- scheduleComponentCreation();\r
- }\r
-\r
- public void waitUntilPopulated() {\r
- if (populated.get())\r
- return;\r
-\r
- try {\r
- boolean done = false;\r
- while (!done) {\r
- done = populationSemaphore.tryAcquire(10, TimeUnit.MILLISECONDS);\r
- while (!done && getDisplay().readAndDispatch()) {\r
- /*\r
- * Note: readAndDispatch can cause this to be disposed.\r
- */\r
- if(isDisposed()) return;\r
- done = populationSemaphore.tryAcquire();\r
- }\r
- }\r
- } catch (InterruptedException e) {\r
- throw new Error("EmbeddedSwingComposite population interrupted for class " + this, e);\r
- }\r
- }\r
-\r
- /**\r
- * Returns the embedded AWT frame. The returned frame is the root of the AWT containment\r
- * hierarchy for the embedded Swing component. This method can be called from \r
- * any thread. \r
- * \r
- * @return the embedded frame\r
- */\r
- public Frame getFrame() {\r
- // Intentionally leaving out checkWidget() call. This may need to be called from within user's \r
- // createSwingComponent() method. Accessing from a non-SWT thread is OK, but we still check\r
- // for disposal\r
- if (getDisplay() == null || isDisposed()) {\r
- SWT.error(SWT.ERROR_WIDGET_DISPOSED);\r
- }\r
- AwtContext ctx = awtContext;\r
- return (ctx != null) ? ctx.getFrame() : null;\r
- }\r
-\r
- private void createFrame() {\r
- assert Display.getCurrent() != null; // On SWT event thread\r
-\r
- // Make sure Awt environment is initialized. \r
- AwtEnvironment.getInstance(getDisplay());\r
-\r
- if (awtContext != null) {\r
- final Frame oldFrame = awtContext.getFrame();\r
- // Schedule disposal of old frame on AWT thread so that there are no problems with\r
- // already-scheduled operations that have not completed.\r
- // Note: the implementation of Frame.dispose() would schedule the use of the AWT \r
- // thread even if it was not done here, but it uses invokeAndWait() which is \r
- // prone to deadlock (and not necessary for this case). \r
- EventQueue.invokeLater(new Runnable() {\r
- public void run() {\r
- oldFrame.dispose();\r
- }\r
- });\r
- }\r
- Frame frame = SWT_AWT.new_Frame(this);\r
- awtContext = new AwtContext(frame);\r
-\r
- // See Simantics issue #3518\r
- workaroundJava7FocusProblem(frame);\r
-\r
- // Glue the two frameworks together. Do this before anything is added to the frame\r
- // so that all necessary listeners are in place.\r
- createFocusHandlers();\r
-\r
- // This listener clears garbage during resizing, making it looker much cleaner \r
- addControlListener(new CleanResizeListener());\r
- }\r
-\r
- private void workaroundJava7FocusProblem(Frame frame) {\r
- String ver = System.getProperty("java.version");\r
- if (ver.startsWith("1.7") || ver.startsWith("1.8")) {\r
- try {\r
- frame.addWindowListener(new Java7FocusFixListener(this, frame));\r
- } catch (SecurityException e) {\r
- Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));\r
- } catch (NoSuchMethodException e) {\r
- Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));\r
- }\r
- }\r
- }\r
-\r
- static class Java7FocusFixListener extends WindowAdapter {\r
-\r
- Method shellSetActiveControl;\r
- Control control;\r
- Frame frame;\r
-\r
- public Java7FocusFixListener(Control control, Frame frame) throws NoSuchMethodException, SecurityException {\r
- this.shellSetActiveControl = Shell.class.getDeclaredMethod("setActiveControl", Control.class);\r
- this.frame = frame;\r
- this.control = control;\r
- }\r
-\r
- @Override\r
- public void windowActivated(WindowEvent e) {\r
- SWTUtils.asyncExec(control, new Runnable() {\r
- @Override\r
- public void run() {\r
- if (control.isDisposed())\r
- return;\r
- if (control.getDisplay().getFocusControl() == control) {\r
- try {\r
- boolean accessible = shellSetActiveControl.isAccessible();\r
- if (!accessible)\r
- shellSetActiveControl.setAccessible(true);\r
- shellSetActiveControl.invoke(control.getShell(), control);\r
- if (!accessible)\r
- shellSetActiveControl.setAccessible(false);\r
- } catch (SecurityException e) {\r
- Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));\r
- } catch (IllegalArgumentException e) {\r
- Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));\r
- } catch (IllegalAccessException e) {\r
- Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));\r
- } catch (InvocationTargetException e) {\r
- Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getCause().getMessage(), e.getCause()));\r
- }\r
- }\r
- }\r
- });\r
- }\r
-\r
- }\r
-\r
- private void createFocusHandlers() {\r
- assert awtContext != null;\r
- assert Display.getCurrent() != null; // On SWT event thread\r
-\r
- Frame frame = awtContext.getFrame();\r
- awtHandler = new AwtFocusHandler(frame); \r
- SwtFocusHandler swtHandler = new SwtFocusHandler(this);\r
- awtHandler.setSwtHandler(swtHandler);\r
- swtHandler.setAwtHandler(awtHandler);\r
-\r
- // Ensure that AWT pop-ups are dismissed whenever a SWT menu is shown\r
- getDisplay().addFilter(SWT.Show, menuListener);\r
-\r
- EmbeddedChildFocusTraversalPolicy policy = new EmbeddedChildFocusTraversalPolicy(awtHandler);\r
- frame.setFocusTraversalPolicy(policy);\r
- }\r
-\r
- private void scheduleComponentCreation() {\r
- assert awtContext != null;\r
-\r
- // Create AWT/Swing components on the AWT thread. This is \r
- // especially necessary to avoid an AWT leak bug (6411042).\r
- final AwtContext currentContext = awtContext;\r
- ThreadUtils.asyncExec(AWTThread.getThreadAccess(), new Runnable() {\r
- @Override\r
- public void run() {\r
- // Make sure AWT focus fix is in place.\r
- initAWTEventListener();\r
-\r
- panel = addRootPaneContainer(currentContext.getFrame());\r
- panel.setLayout(new GridLayout(1,1,0,0));\r
- try {\r
- Component swingComponent = createSwingComponent();\r
- currentContext.setSwingComponent(swingComponent);\r
- panel.getRootPane().getContentPane().add(swingComponent);\r
- //panel.add(swingComponent);\r
- setComponentFont();\r
- } finally {\r
- // Needed to support #waitUntilPopulated\r
- populated.set(true);\r
- if (populationSemaphore != null)\r
- populationSemaphore.release();\r
- if (populatedCallback != null) {\r
- populatedCallback.accept(SWTAWTComponent.this);\r
- populatedCallback = null;\r
- }\r
- }\r
- }\r
- });\r
- }\r
-\r
- /**\r
- * Adds a root pane container to the embedded AWT frame. Override this to provide your own \r
- * {@link javax.swing.RootPaneContainer} implementation. In most cases, it is not necessary\r
- * to override this method. \r
- * <p>\r
- * This method is called from the AWT event thread. \r
- * <p> \r
- * If you are defining your own root pane container, make sure that there is at least one\r
- * heavyweight (AWT) component in the frame's containment hierarchy; otherwise, event \r
- * processing will not work correctly. See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4982522\r
- * for more information. \r
- * \r
- * @param frame the frame to which the root pane container is added \r
- * @return a non-null Swing component\r
- */\r
- protected JApplet addRootPaneContainer(Frame frame) {\r
- assert EventQueue.isDispatchThread(); // On AWT event thread\r
- assert frame != null;\r
-\r
- // It is important to set up the proper top level components in the frame:\r
- // 1) For Swing to work properly, Sun documents that there must be an implementor of \r
- // javax.swing.RootPaneContainer at the top of the component hierarchy. \r
- // 2) For proper event handling there must be a heavyweight \r
- // an AWT frame must contain a heavyweight component (see \r
- // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4982522)\r
- // 3) The Swing implementation further narrows the options by expecting that the \r
- // top of the hierarchy be a JFrame, JDialog, JWindow, or JApplet. See javax.swing.PopupFactory.\r
- // All this drives the choice of JApplet for the top level Swing component. It is the \r
- // only single component that satisfies all the above. This does not imply that \r
- // we have a true applet; in particular, there is no notion of an applet lifecycle in this\r
- // context. \r
- JApplet applet = new JApplet();\r
- \r
- // In JRE 1.4, the JApplet makes itself a focus cycle root. This\r
- // interferes with the focus handling installed on the parent frame, so\r
- // change it back to a non-root here. \r
- // TODO: consider moving the focus policy from the Frame down to the JApplet\r
- applet.setFocusCycleRoot(false);\r
-\r
- frame.add(applet);\r
-\r
- return applet;\r
- }\r
-\r
- /**\r
- * Override this to customize what kind of AWT/Swing UI is created by this\r
- * {@link SWTAWTComponent}.\r
- * \r
- * @return the AWT/Swing component created by this SWTAWT bridging control\r
- * @thread AWT\r
- */\r
- protected abstract Component createSwingComponent();\r
-\r
- private void setComponentFont() {\r
- assert currentSystemFont != null;\r
- assert EventQueue.isDispatchThread(); // On AWT event thread\r
-\r
- Component swingComponent = (awtContext != null) ? awtContext.getSwingComponent() : null;\r
- if ((swingComponent != null) && !currentSystemFont.getDevice().isDisposed()) {\r
- FontData fontData = currentSystemFont.getFontData()[0];\r
- \r
- // AWT font sizes assume a 72 dpi resolution, always. The true screen resolution must be \r
- // used to convert the platform font size into an AWT point size that matches when displayed. \r
- int resolution = Toolkit.getDefaultToolkit().getScreenResolution();\r
- int awtFontSize = (int)Math.round((double)fontData.getHeight() * resolution / 72.0);\r
- \r
- // The style constants for SWT and AWT map exactly, and since they are int constants, they should\r
- // never change. So, the SWT style is passed through as the AWT style. \r
- java.awt.Font awtFont = new java.awt.Font(fontData.getName(), fontData.getStyle(), awtFontSize);\r
-\r
- // Update the look and feel defaults to use new font.\r
- updateLookAndFeel(awtFont);\r
-\r
- // Allow subclasses to react to font change if necessary. \r
- updateAwtFont(awtFont);\r
-\r
- // Allow components to update their UI based on new font \r
- // TODO: should the update method be called on the root pane instead?\r
- Container contentPane = SwingUtilities.getRootPane(swingComponent).getContentPane();\r
- SwingUtilities.updateComponentTreeUI(contentPane);\r
- }\r
- }\r
-\r
- private void updateLookAndFeel(java.awt.Font awtFont) {\r
- assert awtFont != null;\r
- assert EventQueue.isDispatchThread(); // On AWT event thread\r
-\r
- // The FontUIResource class marks the font as replaceable by the look and feel \r
- // implementation if font settings are later changed. \r
- FontUIResource fontResource = new FontUIResource(awtFont);\r
-\r
- // Assign the new font to the relevant L&F font properties. These are \r
- // the properties that are initially assigned to the system font\r
- // under the Windows look and feel. \r
- // TODO: It's possible that other platforms will need other assignments.\r
- // TODO: This does not handle fonts other than the "system" font. \r
- // Other fonts may change, and the Swing L&F may not be adjusting.\r
-\r
- UIManager.put("Button.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("CheckBox.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("ComboBox.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("EditorPane.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("Label.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("List.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("Panel.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("ProgressBar.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("RadioButton.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("ScrollPane.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("TabbedPane.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("Table.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("TableHeader.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("TextField.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("TextPane.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("TitledBorder.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("ToggleButton.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("TreeFont.font", fontResource); //$NON-NLS-1$\r
- UIManager.put("ViewportFont.font", fontResource); //$NON-NLS-1$\r
- }\r
-\r
- /**\r
- * Performs custom updates to newly set fonts. This method is called whenever a change\r
- * to the system font through the system settings (i.e. control panel) is detected.\r
- * <p>\r
- * This method is called from the AWT event thread. \r
- * <p>\r
- * In most cases it is not necessary to override this method. Normally, the implementation\r
- * of this class will automatically propogate font changes to the embedded Swing components \r
- * through Swing's Look and Feel support. However, if additional \r
- * special processing is necessary, it can be done inside this method. \r
- * \r
- * @param newFont New AWT font\r
- */\r
- protected void updateAwtFont(java.awt.Font newFont) {\r
- }\r
-\r
- private void handleSettingsChange() {\r
- Font newFont = getDisplay().getSystemFont();\r
- if (!newFont.equals(currentSystemFont)) { \r
- currentSystemFont = newFont;\r
- EventQueue.invokeLater(new Runnable() {\r
- public void run() {\r
- setComponentFont();\r
- }\r
- });\r
- }\r
- }\r
-\r
-}\r
+/*******************************************************************************
+ * 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;
+
+
+/**
+ * <pre>
+ * 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.
+ * </pre>
+ * <p>
+ *
+ * @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<SWTAWTComponent> 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<SWTAWTComponent> 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.
+ * <p>
+ * This method is called from the AWT event thread.
+ * <p>
+ * 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.
+ * <p>
+ * This method is called from the AWT event thread.
+ * <p>
+ * 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();
+ }
+ });
+ }
+ }
+
+}