]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.utils.ui/src/org/simantics/utils/ui/SWTAWTComponent.java
8031a93abb4f6728feb6a1afbfee586feece68ea
[simantics/platform.git] / bundles / org.simantics.utils.ui / src / org / simantics / utils / ui / SWTAWTComponent.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2013 Association for Decentralized Information Management
3  * in Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *     Semantum Oy - workaround for Simantics issue #3518
12  *******************************************************************************/
13 package org.simantics.utils.ui;
14
15 import java.awt.AWTEvent;
16 import java.awt.Component;
17 import java.awt.Container;
18 import java.awt.EventQueue;
19 import java.awt.Frame;
20 import java.awt.GridLayout;
21 import java.awt.Toolkit;
22 import java.awt.event.AWTEventListener;
23 import java.awt.event.MouseEvent;
24 import java.awt.event.WindowAdapter;
25 import java.awt.event.WindowEvent;
26 import java.lang.reflect.InvocationTargetException;
27 import java.lang.reflect.Method;
28 import java.util.concurrent.Semaphore;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.atomic.AtomicBoolean;
31 import java.util.function.Consumer;
32
33 import javax.swing.JApplet;
34 import javax.swing.SwingUtilities;
35 import javax.swing.UIManager;
36 import javax.swing.plaf.FontUIResource;
37
38 import org.eclipse.core.runtime.IStatus;
39 import org.eclipse.core.runtime.Status;
40 import org.eclipse.swt.SWT;
41 import org.eclipse.swt.awt.SWT_AWT;
42 import org.eclipse.swt.events.DisposeEvent;
43 import org.eclipse.swt.events.DisposeListener;
44 import org.eclipse.swt.graphics.Font;
45 import org.eclipse.swt.graphics.FontData;
46 import org.eclipse.swt.layout.FillLayout;
47 import org.eclipse.swt.widgets.Composite;
48 import org.eclipse.swt.widgets.Control;
49 import org.eclipse.swt.widgets.Display;
50 import org.eclipse.swt.widgets.Event;
51 import org.eclipse.swt.widgets.Listener;
52 import org.eclipse.swt.widgets.Shell;
53 import org.simantics.utils.threads.AWTThread;
54 import org.simantics.utils.threads.ThreadUtils;
55 import org.simantics.utils.threads.logger.ITask;
56 import org.simantics.utils.threads.logger.ThreadLogger;
57 import org.simantics.utils.ui.internal.Activator;
58 import org.simantics.utils.ui.internal.awt.AwtEnvironment;
59 import org.simantics.utils.ui.internal.awt.AwtFocusHandler;
60 import org.simantics.utils.ui.internal.awt.CleanResizeListener;
61 import org.simantics.utils.ui.internal.awt.EmbeddedChildFocusTraversalPolicy;
62 import org.simantics.utils.ui.internal.awt.SwtFocusHandler;
63
64
65 /**
66  * <pre>
67  *        embeddedComposite = new SWTAWTComposite(parent, SWT.NONE) {
68  *            protected JComponent createSwingComponent() {
69  *                scrollPane = new JScrollPane();
70  *                table = new JTable();
71  *                scrollPane.setViewportView(table);
72  *                return scrollPane;
73  *            }
74  *        };
75  *        // For asynchronous AWT UI population of the swing components:
76  *        embeddedComposite.populate();
77  *        // and optionally you can wait until the AWT UI population
78  *        // has finished:
79  *        embeddedComposite.waitUntilPopulated();
80  *
81  *        // OR:
82  *
83  *        // Do both things above in one call to block until the
84  *        // AWT UI population is complete:
85  *        embeddedComposite.syncPopulate();
86  *
87  *        // OR:
88  *
89  *        // Set a callback for asynchronous completion in the AWT thread:
90  *        embeddedComposite.populate(component -> {
91               // AWT components have been created for component
92  *        });
93  *
94  *        // All methods assume all invocations are made from the SWT display thread.
95  * </pre>
96  * <p>
97  * 
98  * @author Tuukka Lehtonen
99  */
100 public abstract class SWTAWTComponent extends Composite {
101
102     private static class AwtContext {
103         private Frame frame;
104         private Component swingComponent;
105
106         AwtContext(Frame frame) {
107             assert frame != null;
108             this.frame = frame;
109         }
110
111         Frame getFrame() {
112             return frame;
113         }
114
115         void setSwingComponent(Component swingComponent) {
116             this.swingComponent = swingComponent;
117         }
118
119         Component getSwingComponent() {
120             return swingComponent;
121         }
122
123     }
124
125     private Font                    currentSystemFont;
126     private AwtContext              awtContext;
127     private AwtFocusHandler         awtHandler;
128
129     private JApplet                 panel;
130
131     private final AtomicBoolean     populationStarted   = new AtomicBoolean(false);
132
133     private final AtomicBoolean     populated           = new AtomicBoolean(false);
134
135     private final Semaphore         populationSemaphore = new Semaphore(0);
136
137     private Consumer<SWTAWTComponent> populatedCallback;
138
139     private static AWTEventListener awtListener         = null;
140
141     private Listener settingsListener = new Listener() {
142         public void handleEvent(Event event) {
143             handleSettingsChange();
144         }
145     };
146
147     // This listener helps ensure that Swing popup menus are properly dismissed when
148     // a menu item off the SWT main menu bar is shown.
149     private final Listener menuListener = new Listener() {
150         public void handleEvent(Event event) {
151             assert awtHandler != null;
152             awtHandler.postHidePopups();
153         }
154     };
155
156     public SWTAWTComponent(Composite parent, int style) {
157         super(parent, style | SWT.NO_BACKGROUND | SWT.NO_REDRAW_RESIZE | SWT.EMBEDDED);
158         getDisplay().addListener(SWT.Settings, settingsListener);
159         setLayout(new FillLayout());
160         currentSystemFont = getFont();
161         this.addDisposeListener(new DisposeListener() {
162             @Override
163             public void widgetDisposed(DisposeEvent e) {
164                 doDispose();
165             }
166         });
167     }
168
169     protected void doDispose() {
170         getDisplay().removeListener(SWT.Settings, settingsListener);
171         getDisplay().removeFilter(SWT.Show, menuListener);
172
173         ThreadUtils.asyncExec(AWTThread.getThreadAccess(), new Runnable() {
174             @Override
175             public void run() {
176                 AwtContext ctx = awtContext;
177                 if (ctx != null) {
178                     ctx.frame.dispose();
179                 }
180                 awtContext = null;
181                 if (panel != null) {
182                     panel.removeAll();
183                     panel = null;
184                 }
185             }
186         });
187     }
188
189     static class FocusRepairListener implements AWTEventListener {
190         @Override
191         public void eventDispatched(AWTEvent e) {
192             if (e.getID() == MouseEvent.MOUSE_PRESSED) {
193                 Object src = e.getSource();
194                 if (src instanceof Component) {
195                     ((Component) src).requestFocus();
196                 }
197             }
198         }
199     }
200
201     /**
202      * Create a global AWTEventListener for focus management.
203      * This helps at least with Linux/GTK problems of transferring focus
204      * to workbench parts when clicking on AWT screen territory.
205      * 
206      * NOTE: There is really no need to dispose this once it's been initialized.
207      * 
208      * NOTE: must be invoked from AWT thread.
209      */
210     private static synchronized void initAWTEventListener() {
211         if (!AWTThread.getThreadAccess().currentThreadAccess())
212             throw new AssertionError("not invoked from AWT thread");
213         if (awtListener == null) {
214             awtListener = new FocusRepairListener();
215             Toolkit.getDefaultToolkit().addAWTEventListener(awtListener, AWTEvent.MOUSE_EVENT_MASK);
216         }
217     }
218
219     protected Container getContainer() {
220         return panel;
221     }
222
223     public Component getAWTComponent() {
224         assert awtContext != null;
225         return awtContext.getSwingComponent();
226     }
227
228     /**
229      * This method must always be called from SWT thread. This method should be
230      * used with extreme care since it will block the calling thread (i.e. the
231      * SWT thread) while the AWT thread initializes itself by spinning and
232      * dispatching SWT events. This diminishes the possibility of deadlock
233      * (reported between AWT and SWT) but still all UI's are recommended to use
234      * the asynchronous non-blocking UI population offered by
235      * {@link #populate(Consumer)}
236      * 
237      * @see #populate(Consumer)
238      */
239     public void syncPopulate() {
240         populate();
241         waitUntilPopulated();
242     }
243
244     /**
245      * This method must always be called from SWT thread. This will schedule the
246      * real AWT component creation into the AWT thread and call the provided
247      * asynchronous callback after the UI population is complete.
248      * This prevents the possibility of deadlocking.
249      */
250     public void populate(Consumer<SWTAWTComponent> callback) {
251         populate();
252         this.populatedCallback = callback;
253     }
254
255     /**
256      * This method will create an AWT {@link Frame} through {@link SWT_AWT} and
257      * schedule AWT canvas initialization into the AWT thread. It will not wait
258      * for AWT initialization to complete.
259      */
260     public void populate() {
261         if (!populationStarted.compareAndSet(false, true))
262             throw new IllegalStateException(this + ".populate was invoked multiple times");
263
264         checkWidget();
265         ITask task = ThreadLogger.getInstance().begin("createFrame");
266         createFrame();
267         task.finish();
268         scheduleComponentCreation();
269     }
270
271     public void waitUntilPopulated() {
272         if (populated.get())
273             return;
274
275         try {
276             boolean done = false;
277             while (!done) {
278                 done = populationSemaphore.tryAcquire(10, TimeUnit.MILLISECONDS);
279                 while (!done && getDisplay().readAndDispatch()) {
280                     /*
281                      * Note: readAndDispatch can cause this to be disposed.
282                      */
283                     if(isDisposed()) return;
284                     done = populationSemaphore.tryAcquire();
285                 }
286             }
287         } catch (InterruptedException e) {
288             throw new Error("EmbeddedSwingComposite population interrupted for class " + this, e);
289         }
290     }
291
292     /**
293      * Returns the embedded AWT frame. The returned frame is the root of the AWT containment
294      * hierarchy for the embedded Swing component. This method can be called from 
295      * any thread. 
296      *    
297      * @return the embedded frame
298      */
299     public Frame getFrame() {
300         // Intentionally leaving out checkWidget() call. This may need to be called from within user's 
301         // createSwingComponent() method. Accessing from a non-SWT thread is OK, but we still check
302         // for disposal
303         if (getDisplay() == null || isDisposed()) {
304             SWT.error(SWT.ERROR_WIDGET_DISPOSED);
305         }
306         AwtContext ctx = awtContext;
307         return (ctx != null) ? ctx.getFrame() : null;
308     }
309
310     private void createFrame() {
311         assert Display.getCurrent() != null;     // On SWT event thread
312
313         // Make sure Awt environment is initialized. 
314         AwtEnvironment.getInstance(getDisplay());
315
316         if (awtContext != null) {
317             final Frame oldFrame = awtContext.getFrame();
318             // Schedule disposal of old frame on AWT thread so that there are no problems with
319             // already-scheduled operations that have not completed.
320             // Note: the implementation of Frame.dispose() would schedule the use of the AWT 
321             // thread even if it was not done here, but it uses invokeAndWait() which is 
322             // prone to deadlock (and not necessary for this case). 
323             EventQueue.invokeLater(new Runnable() {
324                 public void run() {
325                     oldFrame.dispose();
326                 }
327             });
328         }
329         Frame frame = SWT_AWT.new_Frame(this);
330         awtContext = new AwtContext(frame);
331
332         // See Simantics issue #3518
333         workaroundJava7FocusProblem(frame);
334
335         // Glue the two frameworks together. Do this before anything is added to the frame
336         // so that all necessary listeners are in place.
337         createFocusHandlers();
338
339         // This listener clears garbage during resizing, making it looker much cleaner 
340         addControlListener(new CleanResizeListener());
341     }
342
343     private void workaroundJava7FocusProblem(Frame frame) {
344         String ver = System.getProperty("java.version");
345         if (ver.startsWith("1.7") || ver.startsWith("1.8")) {
346             try {
347                 frame.addWindowListener(new Java7FocusFixListener(this, frame));
348             } catch (SecurityException e) {
349                 Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));
350             } catch (NoSuchMethodException e) {
351                 Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));
352             }
353         }
354     }
355
356     static class Java7FocusFixListener extends WindowAdapter {
357
358         Method shellSetActiveControl;
359         Control control;
360         Frame frame;
361
362         public Java7FocusFixListener(Control control, Frame frame) throws NoSuchMethodException, SecurityException {
363             this.shellSetActiveControl = Shell.class.getDeclaredMethod("setActiveControl", Control.class);
364             this.frame = frame;
365             this.control = control;
366         }
367
368         @Override
369         public void windowActivated(WindowEvent e) {
370             SWTUtils.asyncExec(control, new Runnable() {
371                 @Override
372                 public void run() {
373                     if (control.isDisposed())
374                         return;
375                     if (control.getDisplay().getFocusControl() == control) {
376                         try {
377                             boolean accessible = shellSetActiveControl.isAccessible();
378                             if (!accessible)
379                                 shellSetActiveControl.setAccessible(true);
380                             shellSetActiveControl.invoke(control.getShell(), control);
381                             if (!accessible)
382                                 shellSetActiveControl.setAccessible(false);
383                         } catch (SecurityException e) {
384                             Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));
385                         } catch (IllegalArgumentException e) {
386                             Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));
387                         } catch (IllegalAccessException e) {
388                             Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));
389                         } catch (InvocationTargetException e) {
390                             Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getCause().getMessage(), e.getCause()));
391                         }
392                     }
393                 }
394             });
395         }
396
397     }
398
399     private void createFocusHandlers() {
400         assert awtContext != null;
401         assert Display.getCurrent() != null;     // On SWT event thread
402
403         Frame frame = awtContext.getFrame();
404         awtHandler = new AwtFocusHandler(frame);   
405         SwtFocusHandler swtHandler = new SwtFocusHandler(this);
406         awtHandler.setSwtHandler(swtHandler);
407         swtHandler.setAwtHandler(awtHandler);
408
409         // Ensure that AWT pop-ups are dismissed whenever a SWT menu is shown
410         getDisplay().addFilter(SWT.Show, menuListener);
411
412         EmbeddedChildFocusTraversalPolicy policy = new EmbeddedChildFocusTraversalPolicy(awtHandler);
413         frame.setFocusTraversalPolicy(policy);
414     }
415
416     private void scheduleComponentCreation() {
417         assert awtContext != null;
418
419         // Create AWT/Swing components on the AWT thread. This is 
420         // especially necessary to avoid an AWT leak bug (6411042).
421         final AwtContext currentContext = awtContext;
422         ThreadUtils.asyncExec(AWTThread.getThreadAccess(), new Runnable() {
423             @Override
424             public void run() {
425                 // Make sure AWT focus fix is in place.
426                 initAWTEventListener();
427
428                 panel = addRootPaneContainer(currentContext.getFrame());
429                 panel.setLayout(new GridLayout(1,1,0,0));
430                 try {
431                     Component swingComponent = createSwingComponent();
432                     currentContext.setSwingComponent(swingComponent);
433                     panel.getRootPane().getContentPane().add(swingComponent);
434                     //panel.add(swingComponent);
435                     setComponentFont();
436                 } finally {
437                     // Needed to support #waitUntilPopulated
438                     populated.set(true);
439                     if (populationSemaphore != null)
440                         populationSemaphore.release();
441                     if (populatedCallback != null) {
442                         populatedCallback.accept(SWTAWTComponent.this);
443                         populatedCallback = null;
444                     }
445                 }
446             }
447         });
448     }
449
450     /**
451      * Adds a root pane container to the embedded AWT frame. Override this to provide your own 
452      * {@link javax.swing.RootPaneContainer} implementation. In most cases, it is not necessary
453      * to override this method.    
454      * <p>
455      * This method is called from the AWT event thread. 
456      * <p> 
457      * If you are defining your own root pane container, make sure that there is at least one
458      * heavyweight (AWT) component in the frame's containment hierarchy; otherwise, event 
459      * processing will not work correctly. See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4982522
460      * for more information.  
461      *   
462      * @param frame the frame to which the root pane container is added 
463      * @return a non-null Swing component
464      */
465     protected JApplet addRootPaneContainer(Frame frame) {
466         assert EventQueue.isDispatchThread();    // On AWT event thread
467         assert frame != null;
468
469         // It is important to set up the proper top level components in the frame:
470         // 1) For Swing to work properly, Sun documents that there must be an implementor of 
471         // javax.swing.RootPaneContainer at the top of the component hierarchy. 
472         // 2) For proper event handling there must be a heavyweight 
473         // an AWT frame must contain a heavyweight component (see 
474         // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4982522)
475         // 3) The Swing implementation further narrows the options by expecting that the 
476         // top of the hierarchy be a JFrame, JDialog, JWindow, or JApplet. See javax.swing.PopupFactory.
477         // All this drives the choice of JApplet for the top level Swing component. It is the 
478         // only single component that satisfies all the above. This does not imply that 
479         // we have a true applet; in particular, there is no notion of an applet lifecycle in this
480         // context. 
481         JApplet applet = new JApplet();
482         
483         // In JRE 1.4, the JApplet makes itself a focus cycle root. This
484         // interferes with the focus handling installed on the parent frame, so
485         // change it back to a non-root here. 
486         // TODO: consider moving the focus policy from the Frame down to the JApplet
487         applet.setFocusCycleRoot(false);
488
489         frame.add(applet);
490
491         return applet;
492     }
493
494     /**
495      * Override this to customize what kind of AWT/Swing UI is created by this
496      * {@link SWTAWTComponent}.
497      * 
498      * @return the AWT/Swing component created by this SWTAWT bridging control
499      * @thread AWT
500      */
501     protected abstract Component createSwingComponent();
502
503     private void setComponentFont() {
504         assert currentSystemFont != null;
505         assert EventQueue.isDispatchThread();    // On AWT event thread
506
507         Component swingComponent = (awtContext != null) ? awtContext.getSwingComponent() : null;
508         if ((swingComponent != null) && !currentSystemFont.getDevice().isDisposed()) {
509             FontData fontData = currentSystemFont.getFontData()[0];
510             
511             // AWT font sizes assume a 72 dpi resolution, always. The true screen resolution must be 
512             // used to convert the platform font size into an AWT point size that matches when displayed. 
513             int resolution = Toolkit.getDefaultToolkit().getScreenResolution();
514             int awtFontSize = (int)Math.round((double)fontData.getHeight() * resolution / 72.0);
515             
516             // The style constants for SWT and AWT map exactly, and since they are int constants, they should
517             // never change. So, the SWT style is passed through as the AWT style. 
518             java.awt.Font awtFont = new java.awt.Font(fontData.getName(), fontData.getStyle(), awtFontSize);
519
520             // Update the look and feel defaults to use new font.
521             updateLookAndFeel(awtFont);
522
523             // Allow subclasses to react to font change if necessary. 
524             updateAwtFont(awtFont);
525
526             // Allow components to update their UI based on new font 
527             // TODO: should the update method be called on the root pane instead?
528             Container contentPane = SwingUtilities.getRootPane(swingComponent).getContentPane();
529             SwingUtilities.updateComponentTreeUI(contentPane);
530         }
531     }
532
533     private void updateLookAndFeel(java.awt.Font awtFont) {
534         assert awtFont != null;
535         assert EventQueue.isDispatchThread();    // On AWT event thread
536
537         // The FontUIResource class marks the font as replaceable by the look and feel 
538         // implementation if font settings are later changed. 
539         FontUIResource fontResource = new FontUIResource(awtFont);
540
541         // Assign the new font to the relevant L&F font properties. These are 
542         // the properties that are initially assigned to the system font
543         // under the Windows look and feel. 
544         // TODO: It's possible that other platforms will need other assignments.
545         // TODO: This does not handle fonts other than the "system" font. 
546         // Other fonts may change, and the Swing L&F may not be adjusting.
547
548         UIManager.put("Button.font", fontResource); //$NON-NLS-1$
549         UIManager.put("CheckBox.font", fontResource); //$NON-NLS-1$
550         UIManager.put("ComboBox.font", fontResource); //$NON-NLS-1$
551         UIManager.put("EditorPane.font", fontResource); //$NON-NLS-1$
552         UIManager.put("Label.font", fontResource); //$NON-NLS-1$
553         UIManager.put("List.font", fontResource); //$NON-NLS-1$
554         UIManager.put("Panel.font", fontResource); //$NON-NLS-1$
555         UIManager.put("ProgressBar.font", fontResource); //$NON-NLS-1$
556         UIManager.put("RadioButton.font", fontResource); //$NON-NLS-1$
557         UIManager.put("ScrollPane.font", fontResource); //$NON-NLS-1$
558         UIManager.put("TabbedPane.font", fontResource); //$NON-NLS-1$
559         UIManager.put("Table.font", fontResource); //$NON-NLS-1$
560         UIManager.put("TableHeader.font", fontResource); //$NON-NLS-1$
561         UIManager.put("TextField.font", fontResource); //$NON-NLS-1$
562         UIManager.put("TextPane.font", fontResource); //$NON-NLS-1$
563         UIManager.put("TitledBorder.font", fontResource); //$NON-NLS-1$
564         UIManager.put("ToggleButton.font", fontResource); //$NON-NLS-1$
565         UIManager.put("TreeFont.font", fontResource); //$NON-NLS-1$
566         UIManager.put("ViewportFont.font", fontResource); //$NON-NLS-1$
567     }
568
569     /**
570      * Performs custom updates to newly set fonts. This method is called whenever a change
571      * to the system font through the system settings (i.e. control panel) is detected.
572      * <p>
573      * This method is called from the AWT event thread.  
574      * <p>
575      * In most cases it is not necessary to override this method.  Normally, the implementation
576      * of this class will automatically propogate font changes to the embedded Swing components 
577      * through Swing's Look and Feel support. However, if additional 
578      * special processing is necessary, it can be done inside this method. 
579      *    
580      * @param newFont New AWT font
581      */
582     protected void updateAwtFont(java.awt.Font newFont) {
583     }
584
585     private void handleSettingsChange() {
586         Font newFont = getDisplay().getSystemFont();
587         if (!newFont.equals(currentSystemFont)) { 
588             currentSystemFont = newFont;
589             EventQueue.invokeLater(new Runnable() {
590                 public void run() {
591                     setComponentFont();
592                 }
593             });
594         }
595     }
596
597 }