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