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