1 /*******************************************************************************
2 * Copyright (c) 2007, 2013 Association for Decentralized Information Management
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
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;
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;
33 import javax.swing.JApplet;
34 import javax.swing.SwingUtilities;
35 import javax.swing.UIManager;
36 import javax.swing.plaf.FontUIResource;
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;
69 * embeddedComposite = new SWTAWTComposite(parent, SWT.NONE) {
70 * protected JComponent createSwingComponent() {
71 * scrollPane = new JScrollPane();
72 * table = new JTable();
73 * scrollPane.setViewportView(table);
77 * // For asynchronous AWT UI population of the swing components:
78 * embeddedComposite.populate();
79 * // and optionally you can wait until the AWT UI population
81 * embeddedComposite.waitUntilPopulated();
85 * // Do both things above in one call to block until the
86 * // AWT UI population is complete:
87 * embeddedComposite.syncPopulate();
91 * // Set a callback for asynchronous completion in the AWT thread:
92 * embeddedComposite.populate(component -> {
93 // AWT components have been created for component
96 * // All methods assume all invocations are made from the SWT display thread.
100 * @author Tuukka Lehtonen
102 public abstract class SWTAWTComponent extends Composite {
104 private static final Logger LOGGER = LoggerFactory.getLogger(SWTAWTComponent.class);
106 private static class AwtContext {
108 private Component swingComponent;
110 AwtContext(Frame frame) {
111 assert frame != null;
119 void setSwingComponent(Component swingComponent) {
120 this.swingComponent = swingComponent;
123 Component getSwingComponent() {
124 return swingComponent;
129 private Font currentSystemFont;
130 private AwtContext awtContext;
131 private AwtFocusHandler awtHandler;
133 private JApplet panel;
135 private final AtomicBoolean populationStarted = new AtomicBoolean(false);
137 private final AtomicBoolean populated = new AtomicBoolean(false);
139 private final Semaphore populationSemaphore = new Semaphore(0);
141 private Consumer<SWTAWTComponent> populatedCallback;
143 private static AWTEventListener awtListener = null;
145 private Listener settingsListener = new Listener() {
146 public void handleEvent(Event event) {
147 handleSettingsChange();
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();
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() {
167 public void widgetDisposed(DisposeEvent e) {
173 protected void doDispose() {
174 getDisplay().removeListener(SWT.Settings, settingsListener);
175 getDisplay().removeFilter(SWT.Show, menuListener);
177 ThreadUtils.asyncExec(AWTThread.getThreadAccess(), new Runnable() {
180 AwtContext ctx = awtContext;
193 static class FocusRepairListener implements AWTEventListener {
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();
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.
210 * NOTE: There is really no need to dispose this once it's been initialized.
212 * NOTE: must be invoked from AWT thread.
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);
223 protected Container getContainer() {
227 public Component getAWTComponent() {
228 assert awtContext != null;
229 return awtContext.getSwingComponent();
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)}
241 * @see #populate(Consumer)
243 public void syncPopulate() {
245 waitUntilPopulated();
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.
254 public void populate(Consumer<SWTAWTComponent> callback) {
256 this.populatedCallback = callback;
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.
264 public void populate() {
265 if (!populationStarted.compareAndSet(false, true))
266 throw new IllegalStateException(this + ".populate was invoked multiple times");
269 ITask task = ThreadLogger.getInstance().begin("createFrame");
272 scheduleComponentCreation();
275 public void waitUntilPopulated() {
280 boolean done = false;
282 done = populationSemaphore.tryAcquire(10, TimeUnit.MILLISECONDS);
283 while (!done && getDisplay().readAndDispatch()) {
285 * Note: readAndDispatch can cause this to be disposed.
287 if(isDisposed()) return;
288 done = populationSemaphore.tryAcquire();
291 } catch (InterruptedException e) {
292 throw new Error("EmbeddedSwingComposite population interrupted for class " + this, e);
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
301 * @return the embedded frame
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
307 if (getDisplay() == null || isDisposed()) {
308 SWT.error(SWT.ERROR_WIDGET_DISPOSED);
310 AwtContext ctx = awtContext;
311 return (ctx != null) ? ctx.getFrame() : null;
314 private void createFrame() {
315 assert Display.getCurrent() != null; // On SWT event thread
317 // Make sure Awt environment is initialized.
318 AwtEnvironment.getInstance(getDisplay());
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() {
333 Frame frame = SWT_AWT.new_Frame(this);
334 awtContext = new AwtContext(frame);
336 // See Simantics issue #3518
337 workaroundJava7FocusProblem(frame);
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();
343 // This listener clears garbage during resizing, making it looker much cleaner
344 addControlListener(new CleanResizeListener());
347 private void workaroundJava7FocusProblem(Frame frame) {
348 String ver = System.getProperty("java.version");
349 String[] split = ver.split("\\.");
351 if (split.length < 2) {
352 LOGGER.warn("Focus fix listener: unrecognized Java version: " + ver);
357 int major = Integer.parseInt(split[0]);
358 int minor = Integer.parseInt(split[1]);
359 if ((major == 1 && (minor == 7 || minor == 8)) || major >= 9) {
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));
368 } catch (NumberFormatException e) {
369 LOGGER.error("Focus fix listener: unrecognized Java version: " + ver);
373 static class Java7FocusFixListener extends WindowAdapter {
375 Method shellSetActiveControl;
379 public Java7FocusFixListener(Control control, Frame frame) throws NoSuchMethodException, SecurityException {
380 this.shellSetActiveControl = Shell.class.getDeclaredMethod("setActiveControl", Control.class);
382 this.control = control;
386 public void windowActivated(WindowEvent e) {
387 SWTUtils.asyncExec(control, new Runnable() {
390 if (control.isDisposed())
392 if (control.getDisplay().getFocusControl() == control) {
394 boolean accessible = shellSetActiveControl.isAccessible();
396 shellSetActiveControl.setAccessible(true);
397 shellSetActiveControl.invoke(control.getShell(), control);
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()));
416 private void createFocusHandlers() {
417 assert awtContext != null;
418 assert Display.getCurrent() != null; // On SWT event thread
420 Frame frame = awtContext.getFrame();
421 awtHandler = new AwtFocusHandler(frame);
422 SwtFocusHandler swtHandler = new SwtFocusHandler(this);
423 awtHandler.setSwtHandler(swtHandler);
424 swtHandler.setAwtHandler(awtHandler);
426 // Ensure that AWT pop-ups are dismissed whenever a SWT menu is shown
427 getDisplay().addFilter(SWT.Show, menuListener);
429 EmbeddedChildFocusTraversalPolicy policy = new EmbeddedChildFocusTraversalPolicy(awtHandler);
430 frame.setFocusTraversalPolicy(policy);
433 private void scheduleComponentCreation() {
434 assert awtContext != null;
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() {
442 // Make sure AWT focus fix is in place.
443 initAWTEventListener();
445 panel = addRootPaneContainer(currentContext.getFrame());
446 panel.setLayout(new GridLayout(1,1,0,0));
448 Component swingComponent = createSwingComponent();
449 currentContext.setSwingComponent(swingComponent);
450 panel.getRootPane().getContentPane().add(swingComponent);
451 //panel.add(swingComponent);
454 // Needed to support #waitUntilPopulated
456 if (populationSemaphore != null)
457 populationSemaphore.release();
458 if (populatedCallback != null) {
459 populatedCallback.accept(SWTAWTComponent.this);
460 populatedCallback = null;
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.
472 * This method is called from the AWT event thread.
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.
479 * @param frame the frame to which the root pane container is added
480 * @return a non-null Swing component
482 protected JApplet addRootPaneContainer(Frame frame) {
483 assert EventQueue.isDispatchThread(); // On AWT event thread
484 assert frame != null;
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
498 JApplet applet = new JApplet();
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);
512 * Override this to customize what kind of AWT/Swing UI is created by this
513 * {@link SWTAWTComponent}.
515 * @return the AWT/Swing component created by this SWTAWT bridging control
518 protected abstract Component createSwingComponent();
520 private void setComponentFont() {
521 assert currentSystemFont != null;
522 assert EventQueue.isDispatchThread(); // On AWT event thread
524 Component swingComponent = (awtContext != null) ? awtContext.getSwingComponent() : null;
525 if ((swingComponent != null) && !currentSystemFont.getDevice().isDisposed()) {
526 FontData fontData = currentSystemFont.getFontData()[0];
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);
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);
537 // Update the look and feel defaults to use new font.
538 updateLookAndFeel(awtFont);
540 // Allow subclasses to react to font change if necessary.
541 updateAwtFont(awtFont);
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);
550 private void updateLookAndFeel(java.awt.Font awtFont) {
551 assert awtFont != null;
552 assert EventQueue.isDispatchThread(); // On AWT event thread
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);
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.
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$
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.
590 * This method is called from the AWT event thread.
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.
597 * @param newFont New AWT font
599 protected void updateAwtFont(java.awt.Font newFont) {
602 private void handleSettingsChange() {
603 Font newFont = getDisplay().getSystemFont();
604 if (!newFont.equals(currentSystemFont)) {
605 currentSystemFont = newFont;
606 EventQueue.invokeLater(new Runnable() {