]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.utils.ui/src/org/simantics/utils/ui/awt/EmbeddedSwingComposite.java
Sync git svn branch with SVN repository r33406.
[simantics/platform.git] / bundles / org.simantics.utils.ui / src / org / simantics / utils / ui / awt / EmbeddedSwingComposite.java
1 /*******************************************************************************\r
2  * Copyright (c) 2007 SAS Institute.\r
3  * All rights reserved. This program and the accompanying materials\r
4  * are made available under the terms of the Eclipse Public License v1.0\r
5  * which accompanies this distribution, and is available at\r
6  * http://www.eclipse.org/legal/epl-v10.html\r
7  *\r
8  * Contributors:\r
9  *     SAS Institute - initial API and implementation\r
10  *******************************************************************************/\r
11 package org.simantics.utils.ui.awt;\r
12 \r
13 import java.awt.Container;\r
14 import java.awt.EventQueue;\r
15 import java.awt.Frame;\r
16 import java.awt.Toolkit;\r
17 \r
18 import javax.swing.JApplet;\r
19 import javax.swing.JComponent;\r
20 import javax.swing.RootPaneContainer;\r
21 import javax.swing.SwingUtilities;\r
22 import javax.swing.UIManager;\r
23 import javax.swing.plaf.FontUIResource;\r
24 \r
25 import org.eclipse.swt.SWT;\r
26 import org.eclipse.swt.SWTException;\r
27 import org.eclipse.swt.awt.SWT_AWT;\r
28 import org.eclipse.swt.graphics.Font;\r
29 import org.eclipse.swt.graphics.FontData;\r
30 import org.eclipse.swt.layout.FillLayout;\r
31 import org.eclipse.swt.widgets.Composite;\r
32 import org.eclipse.swt.widgets.Display;\r
33 import org.eclipse.swt.widgets.Event;\r
34 import org.eclipse.swt.widgets.Listener;\r
35 import org.eclipse.swt.widgets.Widget;\r
36 \r
37 /**\r
38  * A SWT composite widget for embedding Swing components in a SWT composite within an RCP or standalone-SWT application. The Eclipse platform \r
39  * provides limited support for embedding Swing components through {@link org.eclipse.swt.awt.SWT_AWT}. \r
40  * This class extends that support by \r
41  * <ul>\r
42  * <li>Using the platform-specific system Look and Feel. \r
43  * <li>Ensuring AWT modal dialogs are modal across the SWT application.\r
44  * <li>Reducing flicker, especially on window resizes\r
45  * <li>Allowing Tab Traversal to and from the Embedded Frame\r
46  * <li>Dismissing most Pop-Up Menus when focus leaves the AWT frame.  \r
47  * <li>Synchronizing Font Changes from system settings\r
48  * <li>Working around various AWT/Swing bugs  \r
49  * </ul>\r
50  * <P>\r
51  * If, rather than embedding Swing components, you are integrating with Swing by opening \r
52  * Swing dialogs, see the {@link AwtEnvironment} class. \r
53  * <p>\r
54  * This is an abstract that is normally used by extending it and implementing the {@link #createSwingComponent()} method. For example,  \r
55  * <pre>\r
56  *        embeddedComposite = new EmbeddedSwingComposite(parent, SWT.NONE) {\r
57  *            protected JComponent createSwingComponent() {\r
58  *                scrollPane = new JScrollPane();\r
59  *                table = new JTable();\r
60  *                scrollPane.setViewportView(table);\r
61  *                return scrollPane;\r
62  *            }\r
63  *        }; \r
64  *        embeddedComposite.populate();\r
65  * </pre>\r
66  * <p>\r
67  * The Swing component is created inside a standard Swing containment hierarchy, rooted in \r
68  * a {@link javax.swing.RootPaneContainer}. The root pane container is placed inside an AWT frame, as\r
69  * returned by {@link org.eclipse.swt.awt.SWT_AWT#new_Frame(Composite)} \r
70  * <p>\r
71  * <b>Note:</b> When you mix components from Swing/AWT and SWT toolkits, there will be two UI event threads,\r
72  * one for AWT, one for SWT. Most SWT APIs require that you call them from the SWT thread. Swing \r
73  * has similar restrictions though it does not enforce them as much as SWT.\r
74  * <p>\r
75  * Applications need to be aware of the current thread, and, where necessary, schedule tasks to run \r
76  * on another thread. This has always been required in the pure Swing or SWT environments, but when \r
77  * mixing Swing and SWT, more of this scheduling will be necessary.\r
78  * <p>\r
79  * To schedule work on the AWT event \r
80  * thread, you can use:\r
81  * <ul>\r
82  * <li>{@link javax.swing.SwingUtilities#invokeLater(Runnable)}\r
83  * <li>{@link javax.swing.SwingUtilities#invokeAndWait(Runnable)} \r
84  * </ul>\r
85  * <p>\r
86  * (or similar methods in {@link java.awt.EventQueue})\r
87  * <p>\r
88  * To schedule work on the SWT event thread, use:\r
89  * <ul>\r
90  * <li>{@link org.eclipse.swt.widgets.Display#asyncExec(Runnable)}\r
91  * <li>{@link org.eclipse.swt.widgets.Display#syncExec(Runnable)}\r
92  * </ul>\r
93  * \r
94  * Of course, as in single-toolkit environments, long-running tasks should be offloaded from either UI \r
95  * thread to a background thread. The Eclipse jobs API can be used for this purpose.\r
96  */\r
97 public abstract class EmbeddedSwingComposite extends Composite {\r
98     private static class AwtContext {\r
99         private Frame frame;\r
100         private JComponent swingComponent;\r
101         \r
102         AwtContext(Frame frame) {\r
103             assert frame != null;\r
104             this.frame = frame;\r
105         }\r
106 \r
107         Frame getFrame() {\r
108             return frame;\r
109         }\r
110 \r
111         void setSwingComponent(JComponent swingComponent) {\r
112             this.swingComponent = swingComponent;\r
113         }\r
114 \r
115         JComponent getSwingComponent() {\r
116             return swingComponent;\r
117         }\r
118         \r
119     }\r
120     private Font currentSystemFont;\r
121     private AwtContext awtContext;\r
122     private AwtFocusHandler awtHandler;\r
123 \r
124     private Listener settingsListener = new Listener() {\r
125         public void handleEvent(Event event) {\r
126             handleSettingsChange();\r
127         }\r
128     };\r
129     \r
130     // This listener helps ensure that Swing popup menus are properly dismissed when\r
131     // a menu item off the SWT main menu bar is shown.\r
132     private final Listener menuListener = new Listener() {\r
133         public void handleEvent(Event event) {\r
134             assert awtHandler != null;\r
135             \r
136             awtHandler.postHidePopups();\r
137         }\r
138     };\r
139     \r
140     /**\r
141      * Constructs a new instance of this class given its parent\r
142      * and a style value describing its behavior and appearance.\r
143      * <p>\r
144      * This method must be called from the SWT event thread. \r
145      * <p>\r
146      * The style value is either one of the style constants defined in\r
147      * class <code>SWT</code> which is applicable to instances of this\r
148      * class, or must be built by <em>bitwise OR</em>'ing together \r
149      * (that is, using the <code>int</code> "|" operator) two or more\r
150      * of those <code>SWT</code> style constants. The class description\r
151      * lists the style constants that are applicable to the class.\r
152      * Style bits are also inherited from superclasses.\r
153      * </p>\r
154      * <p>\r
155      * The styles SWT.EMBEDDED and SWT.NO_BACKGROUND will be added\r
156      * to the specified style. Usually, no other style bits are needed.\r
157      *\r
158      * @param parent a widget which will be the parent of the new instance (cannot be null)\r
159      * @param style the style of widget to construct\r
160      *\r
161      * @exception IllegalArgumentException <ul>\r
162      *    <li>ERROR_NULL_ARGUMENT - if the parent is null</li>\r
163      * </ul>\r
164      * @exception SWTException <ul>\r
165      *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the SWT event thread\r
166      * </ul>\r
167      *\r
168      * @see Widget#getStyle\r
169      */\r
170     public EmbeddedSwingComposite(Composite parent, int style) {\r
171         super(parent, style | SWT.EMBEDDED | SWT.NO_BACKGROUND);\r
172         getDisplay().addListener(SWT.Settings, settingsListener);\r
173         setLayout(new FillLayout());\r
174         currentSystemFont = getFont();\r
175     }\r
176 \r
177     /**\r
178      * Populates the embedded composite with the Swing component.\r
179      * <p> \r
180      * This method must be called from the\r
181      * SWT event thread.  \r
182      * <p>\r
183      * The Swing component will be created by calling {@link #createSwingComponent()}. The creation is\r
184      * scheduled asynchronously on the AWT event thread. This method does not wait for completion of this\r
185      * asynchronous task, so it may return before createSwingComponent() is complete.   \r
186      * <p>\r
187      * The Swing component is created inside a standard Swing containment hierarchy, rooted in \r
188      * a {@link javax.swing.RootPaneContainer}. Clients can override {@link #addRootPaneContainer(Frame)}\r
189      * to provide their own root pane container implementation.\r
190      * <p>\r
191      * This method can be called multiple times for a single instance. If an embedded frame exists from \r
192      * a previous call, it is disposed.\r
193      *  \r
194      * @exception SWTException <ul>\r
195      *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>\r
196      *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the SWT event thread\r
197      * </ul>\r
198      */\r
199     public void populate() {\r
200         checkWidget();\r
201         createFrame();\r
202         scheduleComponentCreation();\r
203     }\r
204 \r
205     /**\r
206      * Creates the embedded Swing component. This method is called from the AWT event thread. \r
207      * <p> \r
208      * Implement this method to provide the Swing component that will be shown inside this composite.\r
209      * The returned component will be added to the Swing content pane. At least one component must\r
210      * be created by this method; null is not a valid return value.   \r
211      *   \r
212      * @return a non-null Swing component\r
213      */\r
214     protected abstract JComponent createSwingComponent();\r
215     \r
216     /**\r
217      * Adds a root pane container to the embedded AWT frame. Override this to provide your own \r
218      * {@link javax.swing.RootPaneContainer} implementation. In most cases, it is not necessary\r
219      * to override this method.    \r
220      * <p>\r
221      * This method is called from the AWT event thread. \r
222      * <p> \r
223      * If you are defining your own root pane container, make sure that there is at least one\r
224      * heavyweight (AWT) component in the frame's containment hierarchy; otherwise, event \r
225      * processing will not work correctly. See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4982522\r
226      * for more information.  \r
227      *   \r
228      * @param frame the frame to which the root pane container is added \r
229      * @return a non-null Swing component\r
230      */\r
231     protected RootPaneContainer addRootPaneContainer(Frame frame) {\r
232         assert EventQueue.isDispatchThread();    // On AWT event thread\r
233         assert frame != null;\r
234         \r
235         // It is important to set up the proper top level components in the frame:\r
236         // 1) For Swing to work properly, Sun documents that there must be an implementor of \r
237         // javax.swing.RootPaneContainer at the top of the component hierarchy. \r
238         // 2) For proper event handling there must be a heavyweight \r
239         // an AWT frame must contain a heavyweight component (see \r
240         // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4982522)\r
241         // 3) The Swing implementation further narrows the options by expecting that the \r
242         // top of the hierarchy be a JFrame, JDialog, JWindow, or JApplet. See javax.swing.PopupFactory.\r
243         // All this drives the choice of JApplet for the top level Swing component. It is the \r
244         // only single component that satisfies all the above. This does not imply that \r
245         // we have a true applet; in particular, there is no notion of an applet lifecycle in this\r
246         // context. \r
247         JApplet applet = new JApplet();\r
248         \r
249         // In JRE 1.4, the JApplet makes itself a focus cycle root. This\r
250         // interferes with the focus handling installed on the parent frame, so\r
251         // change it back to a non-root here. \r
252         // TODO: consider moving the focus policy from the Frame down to the JApplet\r
253         applet.setFocusCycleRoot(false);\r
254 \r
255         frame.add(applet);\r
256         \r
257         return applet;\r
258     }\r
259 \r
260     /**\r
261      * Performs custom updates to newly set fonts. This method is called whenever a change\r
262      * to the system font through the system settings (i.e. control panel) is detected.\r
263      * <p>\r
264      * This method is called from the AWT event thread.  \r
265      * <p>\r
266      * In most cases it is not necessary to override this method.  Normally, the implementation\r
267      * of this class will automatically propogate font changes to the embedded Swing components \r
268      * through Swing's Look and Feel support. However, if additional \r
269      * special processing is necessary, it can be done inside this method. \r
270      *    \r
271      * @param newFont New AWT font\r
272      */\r
273     protected void updateAwtFont(java.awt.Font newFont) {\r
274     }\r
275 \r
276     /**\r
277      * Returns the embedded AWT frame. The returned frame is the root of the AWT containment\r
278      * hierarchy for the embedded Swing component. This method can be called from \r
279      * any thread. \r
280      *    \r
281      * @return the embedded frame\r
282      */\r
283     public Frame getFrame() {\r
284         // Intentionally leaving out checkWidget() call. This may need to be called from within user's \r
285         // createSwingComponent() method. Accessing from a non-SWT thread is OK, but we still check\r
286         // for disposal\r
287         if (getDisplay() == null || isDisposed()) {\r
288             SWT.error(SWT.ERROR_WIDGET_DISPOSED);            \r
289         }\r
290         \r
291         return (awtContext != null) ? awtContext.getFrame() : null;\r
292     }\r
293 \r
294     private void createFrame() {\r
295         assert Display.getCurrent() != null;     // On SWT event thread\r
296         \r
297         // Make sure Awt environment is initialized. \r
298         AwtEnvironment.getInstance(getDisplay());\r
299         \r
300         if (awtContext != null) {\r
301             final Frame oldFrame = awtContext.getFrame();\r
302             // Schedule disposal of old frame on AWT thread so that there are no problems with\r
303             // already-scheduled operations that have not completed.\r
304             // Note: the implementation of Frame.dispose() would schedule the use of the AWT \r
305             // thread even if it was not done here, but it uses invokeAndWait() which is \r
306             // prone to deadlock (and not necessary for this case). \r
307             EventQueue.invokeLater(new Runnable() {\r
308                 public void run() {\r
309                     oldFrame.dispose();\r
310                 }\r
311             });\r
312         }\r
313         Frame frame = SWT_AWT.new_Frame(this);\r
314         awtContext = new AwtContext(frame);\r
315 \r
316         // Glue the two frameworks together. Do this before anything is added to the frame\r
317         // so that all necessary listeners are in place.\r
318         createFocusHandlers();\r
319         \r
320         // This listener clears garbage during resizing, making it looker much cleaner \r
321         addControlListener(new CleanResizeListener());\r
322     }\r
323 \r
324     private void createFocusHandlers() {\r
325         assert awtContext != null;\r
326         assert Display.getCurrent() != null;     // On SWT event thread\r
327         \r
328         Frame frame = awtContext.getFrame();\r
329         awtHandler = new AwtFocusHandler(frame);   \r
330         SwtFocusHandler swtHandler = new SwtFocusHandler(this);\r
331         awtHandler.setSwtHandler(swtHandler);\r
332         swtHandler.setAwtHandler(awtHandler);\r
333         \r
334         // Ensure that AWT popups are dimissed whenever a SWT menu is shown\r
335         getDisplay().addFilter(SWT.Show, menuListener);\r
336         \r
337         EmbeddedChildFocusTraversalPolicy policy = new EmbeddedChildFocusTraversalPolicy(awtHandler);\r
338         frame.setFocusTraversalPolicy(policy);\r
339     }\r
340     \r
341     private void scheduleComponentCreation() {\r
342         assert awtContext != null;\r
343         \r
344         // Create AWT/Swing components on the AWT thread. This is \r
345         // especially necessary to avoid an AWT leak bug (6411042).\r
346         final AwtContext currentContext = awtContext;\r
347         EventQueue.invokeLater(new Runnable() {\r
348             public void run() {\r
349                 \r
350                 RootPaneContainer container = addRootPaneContainer(currentContext.getFrame());\r
351                 JComponent swingComponent = createSwingComponent();\r
352                 currentContext.setSwingComponent(swingComponent);\r
353                 container.getRootPane().getContentPane().add(swingComponent);\r
354                 setComponentFont();\r
355             }\r
356         });\r
357     }\r
358 \r
359     private void setComponentFont() {\r
360         assert currentSystemFont != null;\r
361         assert EventQueue.isDispatchThread();    // On AWT event thread\r
362         \r
363         JComponent swingComponent = (awtContext != null) ? awtContext.getSwingComponent() : null;\r
364         if ((swingComponent != null) && !currentSystemFont.getDevice().isDisposed()) {\r
365             FontData fontData = currentSystemFont.getFontData()[0];\r
366             \r
367             // AWT font sizes assume a 72 dpi resolution, always. The true screen resolution must be \r
368             // used to convert the platform font size into an AWT point size that matches when displayed. \r
369             int resolution = Toolkit.getDefaultToolkit().getScreenResolution();\r
370             int awtFontSize = (int)Math.round((double)fontData.getHeight() * resolution / 72.0);\r
371             \r
372             // The style constants for SWT and AWT map exactly, and since they are int constants, they should\r
373             // never change. So, the SWT style is passed through as the AWT style. \r
374             java.awt.Font awtFont = new java.awt.Font(fontData.getName(), fontData.getStyle(), awtFontSize);\r
375 \r
376             // Update the look and feel defaults to use new font.\r
377             updateLookAndFeel(awtFont);\r
378 \r
379             // Allow subclasses to react to font change if necessary. \r
380             updateAwtFont(awtFont);\r
381 \r
382             // Allow components to update their UI based on new font \r
383             // TODO: should the update method be called on the root pane instead?\r
384             Container contentPane = swingComponent.getRootPane().getContentPane();\r
385             SwingUtilities.updateComponentTreeUI(contentPane);\r
386         }\r
387     }\r
388     \r
389     private void updateLookAndFeel(java.awt.Font awtFont) {\r
390         assert awtFont != null;\r
391         assert EventQueue.isDispatchThread();    // On AWT event thread\r
392         \r
393         // The FontUIResource class marks the font as replaceable by the look and feel \r
394         // implementation if font settings are later changed. \r
395         FontUIResource fontResource = new FontUIResource(awtFont);\r
396 \r
397         // Assign the new font to the relevant L&F font properties. These are \r
398         // the properties that are initially assigned to the system font\r
399         // under the Windows look and feel. \r
400         // TODO: It's possible that other platforms will need other assignments.\r
401         // TODO: This does not handle fonts other than the "system" font. \r
402         // Other fonts may change, and the Swing L&F may not be adjusting.\r
403         \r
404         UIManager.put("Button.font", fontResource); //$NON-NLS-1$\r
405         UIManager.put("CheckBox.font", fontResource); //$NON-NLS-1$\r
406         UIManager.put("ComboBox.font", fontResource); //$NON-NLS-1$\r
407         UIManager.put("EditorPane.font", fontResource); //$NON-NLS-1$\r
408         UIManager.put("Label.font", fontResource); //$NON-NLS-1$\r
409         UIManager.put("List.font", fontResource); //$NON-NLS-1$\r
410         UIManager.put("Panel.font", fontResource); //$NON-NLS-1$\r
411         UIManager.put("ProgressBar.font", fontResource); //$NON-NLS-1$\r
412         UIManager.put("RadioButton.font", fontResource); //$NON-NLS-1$\r
413         UIManager.put("ScrollPane.font", fontResource); //$NON-NLS-1$\r
414         UIManager.put("TabbedPane.font", fontResource); //$NON-NLS-1$\r
415         UIManager.put("Table.font", fontResource); //$NON-NLS-1$\r
416         UIManager.put("TableHeader.font", fontResource); //$NON-NLS-1$\r
417         UIManager.put("TextField.font", fontResource); //$NON-NLS-1$\r
418         UIManager.put("TextPane.font", fontResource); //$NON-NLS-1$\r
419         UIManager.put("TitledBorder.font", fontResource); //$NON-NLS-1$\r
420         UIManager.put("ToggleButton.font", fontResource); //$NON-NLS-1$\r
421         UIManager.put("TreeFont.font", fontResource); //$NON-NLS-1$\r
422         UIManager.put("ViewportFont.font", fontResource); //$NON-NLS-1$\r
423     }\r
424 \r
425     private void handleSettingsChange() {\r
426         Font newFont = getDisplay().getSystemFont();\r
427         if (!newFont.equals(currentSystemFont)) { \r
428             currentSystemFont = newFont;\r
429             EventQueue.invokeLater(new Runnable() {\r
430                 public void run() {\r
431                     setComponentFont();\r
432                 }\r
433             });            \r
434         }\r
435     }\r
436 \r
437     private boolean isFocusable() {\r
438         if (awtContext == null) {\r
439             return false;\r
440         }\r
441         JComponent swingComponent = awtContext.getSwingComponent();\r
442         return (swingComponent != null) && swingComponent.isFocusable(); \r
443     }\r
444 \r
445     /* (non-Javadoc)\r
446      * @see org.eclipse.swt.widgets.Control#setFocus()\r
447      */\r
448     public boolean setFocus() {\r
449         checkWidget();\r
450         \r
451         if (!isFocusable()) {\r
452             return false;\r
453         }\r
454         return super.setFocus();\r
455     }\r
456 \r
457     /* (non-Javadoc)\r
458      * @see org.eclipse.swt.widgets.Control#forceFocus()\r
459      */\r
460     public boolean forceFocus() {\r
461         checkWidget();\r
462         \r
463         if (!isFocusable()) {\r
464             return false;\r
465         }\r
466         return super.forceFocus();\r
467     }\r
468 \r
469     /* (non-Javadoc)\r
470      * @see org.eclipse.swt.widgets.Widget#dispose()\r
471      */\r
472     public void dispose() {\r
473         if (!isDisposed()) {\r
474             getDisplay().removeListener(SWT.Settings, settingsListener);\r
475             getDisplay().removeFilter(SWT.Show, menuListener);\r
476             super.dispose();\r
477         }\r
478     }\r
479     \r
480 }\r