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