]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.utils.ui/src/org/simantics/utils/ui/internal/awt/EmbeddedSwingComposite.java
Introduce WrapLayout to replace FlowLayout
[simantics/platform.git] / bundles / org.simantics.utils.ui / src / org / simantics / utils / ui / internal / awt / EmbeddedSwingComposite.java
diff --git a/bundles/org.simantics.utils.ui/src/org/simantics/utils/ui/internal/awt/EmbeddedSwingComposite.java b/bundles/org.simantics.utils.ui/src/org/simantics/utils/ui/internal/awt/EmbeddedSwingComposite.java
new file mode 100644 (file)
index 0000000..886f505
--- /dev/null
@@ -0,0 +1,480 @@
+/*******************************************************************************\r
+ * Copyright (c) 2007 SAS Institute.\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
+ *     SAS Institute - initial API and implementation\r
+ *******************************************************************************/\r
+package org.simantics.utils.ui.internal.awt;\r
+\r
+import java.awt.Container;\r
+import java.awt.EventQueue;\r
+import java.awt.Frame;\r
+import java.awt.Toolkit;\r
+\r
+import javax.swing.JApplet;\r
+import javax.swing.JComponent;\r
+import javax.swing.RootPaneContainer;\r
+import javax.swing.SwingUtilities;\r
+import javax.swing.UIManager;\r
+import javax.swing.plaf.FontUIResource;\r
+\r
+import org.eclipse.swt.SWT;\r
+import org.eclipse.swt.SWTException;\r
+import org.eclipse.swt.awt.SWT_AWT;\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.Display;\r
+import org.eclipse.swt.widgets.Event;\r
+import org.eclipse.swt.widgets.Listener;\r
+import org.eclipse.swt.widgets.Widget;\r
+\r
+/**\r
+ * A SWT composite widget for embedding Swing components in a SWT composite within an RCP or standalone-SWT application. The Eclipse platform \r
+ * provides limited support for embedding Swing components through {@link org.eclipse.swt.awt.SWT_AWT}. \r
+ * This class extends that support by \r
+ * <ul>\r
+ * <li>Using the platform-specific system Look and Feel. \r
+ * <li>Ensuring AWT modal dialogs are modal across the SWT application.\r
+ * <li>Reducing flicker, especially on window resizes\r
+ * <li>Allowing Tab Traversal to and from the Embedded Frame\r
+ * <li>Dismissing most Pop-Up Menus when focus leaves the AWT frame.  \r
+ * <li>Synchronizing Font Changes from system settings\r
+ * <li>Working around various AWT/Swing bugs  \r
+ * </ul>\r
+ * <P>\r
+ * If, rather than embedding Swing components, you are integrating with Swing by opening \r
+ * Swing dialogs, see the {@link AwtEnvironment} class. \r
+ * <p>\r
+ * This is an abstract that is normally used by extending it and implementing the {@link #createSwingComponent()} method. For example,  \r
+ * <pre>\r
+ *        embeddedComposite = new EmbeddedSwingComposite(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
+ *        embeddedComposite.populate();\r
+ * </pre>\r
+ * <p>\r
+ * The Swing component is created inside a standard Swing containment hierarchy, rooted in \r
+ * a {@link javax.swing.RootPaneContainer}. The root pane container is placed inside an AWT frame, as\r
+ * returned by {@link org.eclipse.swt.awt.SWT_AWT#new_Frame(Composite)} \r
+ * <p>\r
+ * <b>Note:</b> When you mix components from Swing/AWT and SWT toolkits, there will be two UI event threads,\r
+ * one for AWT, one for SWT. Most SWT APIs require that you call them from the SWT thread. Swing \r
+ * has similar restrictions though it does not enforce them as much as SWT.\r
+ * <p>\r
+ * Applications need to be aware of the current thread, and, where necessary, schedule tasks to run \r
+ * on another thread. This has always been required in the pure Swing or SWT environments, but when \r
+ * mixing Swing and SWT, more of this scheduling will be necessary.\r
+ * <p>\r
+ * To schedule work on the AWT event \r
+ * thread, you can use:\r
+ * <ul>\r
+ * <li>{@link javax.swing.SwingUtilities#invokeLater(Runnable)}\r
+ * <li>{@link javax.swing.SwingUtilities#invokeAndWait(Runnable)} \r
+ * </ul>\r
+ * <p>\r
+ * (or similar methods in {@link java.awt.EventQueue})\r
+ * <p>\r
+ * To schedule work on the SWT event thread, use:\r
+ * <ul>\r
+ * <li>{@link org.eclipse.swt.widgets.Display#asyncExec(Runnable)}\r
+ * <li>{@link org.eclipse.swt.widgets.Display#syncExec(Runnable)}\r
+ * </ul>\r
+ * \r
+ * Of course, as in single-toolkit environments, long-running tasks should be offloaded from either UI \r
+ * thread to a background thread. The Eclipse jobs API can be used for this purpose.\r
+ */\r
+public abstract class EmbeddedSwingComposite extends Composite {\r
+    private static class AwtContext {\r
+        private Frame frame;\r
+        private JComponent 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(JComponent swingComponent) {\r
+            this.swingComponent = swingComponent;\r
+        }\r
+\r
+        JComponent getSwingComponent() {\r
+            return swingComponent;\r
+        }\r
+        \r
+    }\r
+    private Font currentSystemFont;\r
+    private AwtContext awtContext;\r
+    private AwtFocusHandler awtHandler;\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
+            \r
+            awtHandler.postHidePopups();\r
+        }\r
+    };\r
+    \r
+    /**\r
+     * Constructs a new instance of this class given its parent\r
+     * and a style value describing its behavior and appearance.\r
+     * <p>\r
+     * This method must be called from the SWT event thread. \r
+     * <p>\r
+     * The style value is either one of the style constants defined in\r
+     * class <code>SWT</code> which is applicable to instances of this\r
+     * class, or must be built by <em>bitwise OR</em>'ing together \r
+     * (that is, using the <code>int</code> "|" operator) two or more\r
+     * of those <code>SWT</code> style constants. The class description\r
+     * lists the style constants that are applicable to the class.\r
+     * Style bits are also inherited from superclasses.\r
+     * </p>\r
+     * <p>\r
+     * The styles SWT.EMBEDDED and SWT.NO_BACKGROUND will be added\r
+     * to the specified style. Usually, no other style bits are needed.\r
+     *\r
+     * @param parent a widget which will be the parent of the new instance (cannot be null)\r
+     * @param style the style of widget to construct\r
+     *\r
+     * @exception IllegalArgumentException <ul>\r
+     *    <li>ERROR_NULL_ARGUMENT - if the parent is null</li>\r
+     * </ul>\r
+     * @exception SWTException <ul>\r
+     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the SWT event thread\r
+     * </ul>\r
+     *\r
+     * @see Widget#getStyle\r
+     */\r
+    public EmbeddedSwingComposite(Composite parent, int style) {\r
+        super(parent, style | SWT.EMBEDDED | SWT.NO_BACKGROUND);\r
+        getDisplay().addListener(SWT.Settings, settingsListener);\r
+        setLayout(new FillLayout());\r
+        currentSystemFont = getFont();\r
+    }\r
+\r
+    /**\r
+     * Populates the embedded composite with the Swing component.\r
+     * <p> \r
+     * This method must be called from the\r
+     * SWT event thread.  \r
+     * <p>\r
+     * The Swing component will be created by calling {@link #createSwingComponent()}. The creation is\r
+     * scheduled asynchronously on the AWT event thread. This method does not wait for completion of this\r
+     * asynchronous task, so it may return before createSwingComponent() is complete.   \r
+     * <p>\r
+     * The Swing component is created inside a standard Swing containment hierarchy, rooted in \r
+     * a {@link javax.swing.RootPaneContainer}. Clients can override {@link #addRootPaneContainer(Frame)}\r
+     * to provide their own root pane container implementation.\r
+     * <p>\r
+     * This method can be called multiple times for a single instance. If an embedded frame exists from \r
+     * a previous call, it is disposed.\r
+     *  \r
+     * @exception SWTException <ul>\r
+     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>\r
+     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the SWT event thread\r
+     * </ul>\r
+     */\r
+    public void populate() {\r
+        checkWidget();\r
+        createFrame();\r
+        scheduleComponentCreation();\r
+    }\r
+\r
+    /**\r
+     * Creates the embedded Swing component. This method is called from the AWT event thread. \r
+     * <p> \r
+     * Implement this method to provide the Swing component that will be shown inside this composite.\r
+     * The returned component will be added to the Swing content pane. At least one component must\r
+     * be created by this method; null is not a valid return value.   \r
+     *   \r
+     * @return a non-null Swing component\r
+     */\r
+    protected abstract JComponent createSwingComponent();\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 RootPaneContainer 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
+     * 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
+    /**\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
+        \r
+        return (awtContext != null) ? awtContext.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
+        // 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 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 popups are dimissed 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
+        EventQueue.invokeLater(new Runnable() {\r
+            public void run() {\r
+                \r
+                RootPaneContainer container = addRootPaneContainer(currentContext.getFrame());\r
+                JComponent swingComponent = createSwingComponent();\r
+                currentContext.setSwingComponent(swingComponent);\r
+                container.getRootPane().getContentPane().add(swingComponent);\r
+                setComponentFont();\r
+            }\r
+        });\r
+    }\r
+\r
+    private void setComponentFont() {\r
+        assert currentSystemFont != null;\r
+        assert EventQueue.isDispatchThread();    // On AWT event thread\r
+        \r
+        JComponent 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 = swingComponent.getRootPane().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
+    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
+    private boolean isFocusable() {\r
+        if (awtContext == null) {\r
+            return false;\r
+        }\r
+        JComponent swingComponent = awtContext.getSwingComponent();\r
+        return (swingComponent != null) && swingComponent.isFocusable(); \r
+    }\r
+\r
+    /* (non-Javadoc)\r
+     * @see org.eclipse.swt.widgets.Control#setFocus()\r
+     */\r
+    public boolean setFocus() {\r
+        checkWidget();\r
+        \r
+        if (!isFocusable()) {\r
+            return false;\r
+        }\r
+        return super.setFocus();\r
+    }\r
+\r
+    /* (non-Javadoc)\r
+     * @see org.eclipse.swt.widgets.Control#forceFocus()\r
+     */\r
+    public boolean forceFocus() {\r
+        checkWidget();\r
+        \r
+        if (!isFocusable()) {\r
+            return false;\r
+        }\r
+        return super.forceFocus();\r
+    }\r
+\r
+    /* (non-Javadoc)\r
+     * @see org.eclipse.swt.widgets.Widget#dispose()\r
+     */\r
+    public void dispose() {\r
+        if (!isDisposed()) {\r
+            getDisplay().removeListener(SWT.Settings, settingsListener);\r
+            getDisplay().removeFilter(SWT.Show, menuListener);\r
+            super.dispose();\r
+        }\r
+    }\r
+    \r
+}\r