/******************************************************************************* * Copyright (c) 2008, 2016 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation * Simon Scholz <simon.scholz@vogella.com> - Bug 462056 * Dirk Fauth <dirk.fauth@googlemail.com> - Bug 457939 * Alexander Baranov <achilles-86@mail.ru> - Bug 458460 * Lars Vogel <Lars.Vogel@vogella.com> - Bug 483842 * Patrik Suzzi <psuzzi@gmail.com> - Bug 487621 *******************************************************************************/ package org.eclipse.e4.ui.internal.workbench.swt; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.inject.Named; import org.eclipse.core.databinding.observable.Realm; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.ISafeRunnable; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.core.runtime.preferences.DefaultScope; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.IScopeContext; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.e4.core.contexts.ContextInjectionFactory; import org.eclipse.e4.core.contexts.EclipseContextFactory; import org.eclipse.e4.core.contexts.IEclipseContext; import org.eclipse.e4.core.di.InjectionException; import org.eclipse.e4.core.di.annotations.Optional; import org.eclipse.e4.core.di.extensions.EventTopic; import org.eclipse.e4.core.services.contributions.IContributionFactory; import org.eclipse.e4.core.services.events.IEventBroker; import org.eclipse.e4.core.services.log.Logger; import org.eclipse.e4.core.services.statusreporter.StatusReporter; import org.eclipse.e4.ui.bindings.keys.KeyBindingDispatcher; import org.eclipse.e4.ui.css.core.util.impl.resources.OSGiResourceLocator; import org.eclipse.e4.ui.css.swt.dom.WidgetElement; import org.eclipse.e4.ui.css.swt.engine.CSSSWTEngineImpl; import org.eclipse.e4.ui.css.swt.helpers.EclipsePreferencesHelper; import org.eclipse.e4.ui.css.swt.theme.IThemeEngine; import org.eclipse.e4.ui.css.swt.theme.IThemeManager; import org.eclipse.e4.ui.di.Focus; import org.eclipse.e4.ui.di.PersistState; import org.eclipse.e4.ui.internal.workbench.Activator; import org.eclipse.e4.ui.internal.workbench.E4Workbench; import org.eclipse.e4.ui.model.application.MApplication; import org.eclipse.e4.ui.model.application.MApplicationElement; import org.eclipse.e4.ui.model.application.MContribution; import org.eclipse.e4.ui.model.application.ui.MContext; import org.eclipse.e4.ui.model.application.ui.MElementContainer; import org.eclipse.e4.ui.model.application.ui.MGenericStack; import org.eclipse.e4.ui.model.application.ui.MUIElement; import org.eclipse.e4.ui.model.application.ui.advanced.MPerspective; import org.eclipse.e4.ui.model.application.ui.advanced.MPlaceholder; import org.eclipse.e4.ui.model.application.ui.basic.MPartStack; import org.eclipse.e4.ui.model.application.ui.basic.MTrimmedWindow; import org.eclipse.e4.ui.model.application.ui.basic.MWindow; import org.eclipse.e4.ui.model.application.ui.menu.MMenu; import org.eclipse.e4.ui.model.application.ui.menu.MToolBar; import org.eclipse.e4.ui.services.IStylingEngine; import org.eclipse.e4.ui.workbench.IPresentationEngine; import org.eclipse.e4.ui.workbench.IResourceUtilities; import org.eclipse.e4.ui.workbench.IWorkbench; import org.eclipse.e4.ui.workbench.UIEvents; import org.eclipse.e4.ui.workbench.modeling.EModelService; import org.eclipse.e4.ui.workbench.swt.factories.IRendererFactory; import org.eclipse.emf.ecore.EObject; import org.eclipse.equinox.app.IApplication; import org.eclipse.equinox.app.IApplicationContext; import org.eclipse.jface.bindings.keys.SWTKeySupport; import org.eclipse.jface.bindings.keys.formatting.KeyFormatterFactory; import org.eclipse.jface.databinding.swt.DisplayRealm; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CTabFolder; import org.eclipse.swt.events.ShellAdapter; import org.eclipse.swt.events.ShellEvent; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Widget; import org.eclipse.ui.testing.TestableObject; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.service.event.Event; import org.osgi.service.event.EventHandler; import org.osgi.service.log.LogService; import org.w3c.dom.Element; import org.w3c.dom.css.CSSStyleDeclaration; public class PartRenderingEngine implements IPresentationEngine { public static final String EARLY_STARTUP_HOOK = "runEarlyStartup"; public static final String engineURI = "bundleclass://org.eclipse.e4.ui.workbench.swt/" + "org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine"; private static final String defaultFactoryUrl = "bundleclass://org.eclipse.e4.ui.workbench.renderers.swt/" + "org.eclipse.e4.ui.workbench.renderers.swt.WorkbenchRendererFactory"; public static final String ENABLED_THEME_KEY = "themeEnabled"; private static boolean enableThemePreference; private String factoryUrl; IRendererFactory curFactory = null; private Map<String, AbstractPartRenderer> customRendererMap = new HashMap<>(); org.eclipse.swt.widgets.Listener keyListener; @Inject @Optional private void subscribeTopicToBeRendered(@EventTopic(UIEvents.UIElement.TOPIC_TOBERENDERED) Event event) { MUIElement changedElement = (MUIElement) event.getProperty(UIEvents.EventTags.ELEMENT); MUIElement parent = changedElement.getParent(); // Handle Detached Windows if (parent == null) { parent = (MUIElement) ((EObject) changedElement).eContainer(); } // menus are not handled here... ?? if (parent instanceof MMenu) { return; } // If the parent isn't visible we don't care (The application is // never rendered) boolean okToRender = parent instanceof MApplication || parent.getWidget() != null; if (changedElement.isToBeRendered() && okToRender) { if (Policy.DEBUG_RENDERER) { WorkbenchSWTActivator.trace(Policy.DEBUG_RENDERER_FLAG, "visible -> true", null); //$NON-NLS-1$ } // Note that the 'createGui' protocol calls 'childAdded' Object w = createGui(changedElement); if (w instanceof Control && !(w instanceof Shell)) { fixZOrder(changedElement); } } else { if (Policy.DEBUG_RENDERER) { WorkbenchSWTActivator.trace(Policy.DEBUG_RENDERER_FLAG, "visible -> false", null); //$NON-NLS-1$ } // Ensure that the element about to be removed is not the // selected element if (parent instanceof MElementContainer<?>) { @SuppressWarnings("unchecked") MElementContainer<MUIElement> container = (MElementContainer<MUIElement>) parent; if (container.getSelectedElement() == changedElement) { container.setSelectedElement(null); } } if (okToRender) { // Un-maximize the element before tearing it down if (changedElement.getTags().contains(MAXIMIZED)) { changedElement.getTags().remove(MAXIMIZED); } // Note that the 'removeGui' protocol calls 'childRemoved' removeGui(changedElement); } } } @Inject @Optional private void subscribeVisibilityHandler(@EventTopic(UIEvents.UIElement.TOPIC_VISIBLE) Event event) { MUIElement changedElement = (MUIElement) event.getProperty(UIEvents.EventTags.ELEMENT); MUIElement parent = changedElement.getParent(); if (parent == null) { parent = (MUIElement) ((EObject) changedElement).eContainer(); if (parent == null) { return; } } AbstractPartRenderer renderer = (AbstractPartRenderer) parent.getRenderer(); if (renderer == null || parent instanceof MToolBar) { return; } // Re-parent the control based on the visible state if (changedElement.isVisible()) { if (changedElement.isToBeRendered()) { if (changedElement.getWidget() instanceof Control) { // Ensure that the control is under its 'real' parent if // it's visible Composite realComp = (Composite) renderer.getUIContainer(changedElement); Control ctrl = (Control) changedElement.getWidget(); ctrl.setParent(realComp); fixZOrder(changedElement); } if (parent instanceof MElementContainer<?>) { @SuppressWarnings("unchecked") MElementContainer<MUIElement> container = (MElementContainer<MUIElement>) parent; renderer.childRendered(container, changedElement); } } } else { // Put the control under the 'limbo' shell if (changedElement.getWidget() instanceof Control) { Control ctrl = (Control) changedElement.getWidget(); ctrl.requestLayout(); ctrl.setParent(getLimboShell()); } if (parent instanceof MElementContainer<?>) { @SuppressWarnings("unchecked") MElementContainer<MUIElement> container = (MElementContainer<MUIElement>) parent; renderer.hideChild(container, changedElement); } } } @Inject @Optional private void subscribeTrimHandler(@EventTopic(UIEvents.TrimmedWindow.TOPIC_TRIMBARS) Event event) { Object changedObj = event.getProperty(UIEvents.EventTags.ELEMENT); if (!(changedObj instanceof MTrimmedWindow)) { return; } MTrimmedWindow window = (MTrimmedWindow) changedObj; if (window.getWidget() == null) { return; } if (UIEvents.isADD(event)) { for (Object o : UIEvents.asIterable(event, UIEvents.EventTags.NEW_VALUE)) { MUIElement added = (MUIElement) o; if (added.isToBeRendered()) { createGui(added, window.getWidget(), window.getContext()); } } } else if (UIEvents.isREMOVE(event)) { for (Object o : UIEvents.asIterable(event, UIEvents.EventTags.NEW_VALUE)) { MUIElement removed = (MUIElement) o; if (removed.getRenderer() != null) { removeGui(removed); } } } } @Inject @Optional private void subscribeChildrenHandler(@EventTopic(UIEvents.ElementContainer.TOPIC_CHILDREN) Event event) { Object changedObj = event.getProperty(UIEvents.EventTags.ELEMENT); if (!(changedObj instanceof MElementContainer<?>)) { return; } @SuppressWarnings("unchecked") MElementContainer<MUIElement> changedElement = (MElementContainer<MUIElement>) changedObj; boolean isApplication = changedObj instanceof MApplication; boolean menuChild = changedObj instanceof MMenu; // If the parent isn't in the UI then who cares? AbstractPartRenderer renderer = getRendererFor(changedElement); if ((!isApplication && renderer == null) || menuChild) { return; } if (UIEvents.isADD(event)) { if (Policy.DEBUG_RENDERER) { WorkbenchSWTActivator.trace(Policy.DEBUG_RENDERER_FLAG, "Child Added", null); //$NON-NLS-1$ } for (Object o : UIEvents.asIterable(event, UIEvents.EventTags.NEW_VALUE)) { MUIElement added = (MUIElement) o; // OK, we have a new -visible- part we either have to create // it or host it under the correct parent. Note that we // explicitly do *not* render non-selected elements in // stacks (to support lazy loading). boolean isStack = changedObj instanceof MGenericStack<?>; boolean hasWidget = added.getWidget() != null; boolean isSelected = added == changedElement.getSelectedElement(); boolean renderIt = !isStack || hasWidget || isSelected; if (renderIt) { // NOTE: createGui will call 'childAdded' if successful Object w = createGui(added); if (w instanceof Control && !(w instanceof Shell)) { final Control ctrl = (Control) w; fixZOrder(added); if (!ctrl.isDisposed()) { ctrl.requestLayout(); } } } else { if (renderer != null && added.isToBeRendered()) { renderer.childRendered(changedElement, added); } } // If the element being added is a placeholder, check to see // if // it's 'globally visible' and, if so, remove all other // 'local' placeholders referencing the same element. int newLocation = modelService.getElementLocation(added); if (newLocation == EModelService.IN_SHARED_AREA || newLocation == EModelService.OUTSIDE_PERSPECTIVE) { MWindow topWin = modelService.getTopLevelWindowFor(added); modelService.hideLocalPlaceholders(topWin, null); } } } else if (UIEvents.isREMOVE(event)) { if (Policy.DEBUG_RENDERER) { WorkbenchSWTActivator.trace(Policy.DEBUG_RENDERER_FLAG, "Child Removed", null); //$NON-NLS-1$ } for (Object o : UIEvents.asIterable(event, UIEvents.EventTags.OLD_VALUE)) { MUIElement removed = (MUIElement) o; // Removing invisible elements is a NO-OP as far as the // renderer is concerned if (!removed.isToBeRendered()) { continue; } if (removed.getWidget() instanceof Control) { Control ctrl = (Control) removed.getWidget(); ctrl.setLayoutData(null); // bug 487621 ctrl.getParent().layout(new Control[] { ctrl }, SWT.CHANGED | SWT.DEFER); } // Ensure that the element about to be removed is not the // selected element if (changedElement.getSelectedElement() == removed) { changedElement.setSelectedElement(null); } if (renderer != null) { renderer.hideChild(changedElement, removed); } } } } @Inject @Optional private void subscribeWindowsHandler(@EventTopic(UIEvents.Window.TOPIC_WINDOWS) Event event) { subscribeChildrenHandler(event); } @Inject @Optional private void subscribePerspectiveWindowsHandler(@EventTopic(UIEvents.Perspective.TOPIC_WINDOWS) Event event) { subscribeChildrenHandler(event); } @Inject @Optional private void subscribeCssThemeChanged(@EventTopic(IThemeEngine.Events.THEME_CHANGED) Event event) { cssThemeChangedHandler.handleEvent(event); } private IEclipseContext appContext; protected Shell testShell; protected MApplication theApp; @Inject EModelService modelService; @Inject protected Logger logger; private Shell limbo; private MUIElement removeRoot = null; @Inject @Optional IEventBroker eventBroker; private StylingPreferencesHandler cssThemeChangedHandler; @Inject public PartRenderingEngine( @Named(E4Workbench.RENDERER_FACTORY_URI) @Optional String factoryUrl) { if (factoryUrl == null) { factoryUrl = defaultFactoryUrl; } this.factoryUrl = factoryUrl; } protected void fixZOrder(MUIElement element) { MElementContainer<MUIElement> parent = element.getParent(); if (parent == null) { Object econtainer = ((EObject) element).eContainer(); if (econtainer instanceof MElementContainer<?>) { @SuppressWarnings("unchecked") MElementContainer<MUIElement> container = (MElementContainer<MUIElement>) econtainer; parent = container; } } if (parent == null || !(element.getWidget() instanceof Control)) { return; } Control elementCtrl = (Control) element.getWidget(); Control prevCtrl = null; for (MUIElement kid : parent.getChildren()) { if (kid == element) { if (prevCtrl != null) { elementCtrl.moveBelow(prevCtrl); } else { elementCtrl.moveAbove(null); } break; } else if (kid.getWidget() instanceof Control && kid.isVisible()) { prevCtrl = (Control) kid.getWidget(); } } Object widget = parent.getWidget(); if (widget instanceof Composite) { Composite composite = (Composite) widget; if (composite.getShell() == elementCtrl.getShell()) { Composite temp = elementCtrl.getParent(); while (temp != composite) { if (temp == null) { return; } temp = temp.getParent(); } composite.layout(true, true); } } } /** * Initialize a part renderer from the extension point. * * @param context * the context for the part factories */ @PostConstruct void initialize(IEclipseContext context) { this.appContext = context; // initialize the correct key-binding display formatter KeyFormatterFactory.setDefault(SWTKeySupport.getKeyFormatterForPlatform()); // Add the renderer to the context context.set(IPresentationEngine.class, this); IRendererFactory factory = null; IContributionFactory contribFactory = context.get(IContributionFactory.class); try { factory = (IRendererFactory) contribFactory.create(factoryUrl, context); } catch (Exception e) { logger.warn(e, "Could not create rendering factory"); } // Try to load the default one if (factory == null) { try { factory = (IRendererFactory) contribFactory.create(defaultFactoryUrl, context); } catch (Exception e) { logger.error(e, "Could not create default rendering factory"); } } if (factory == null) { throw new IllegalStateException("Could not create any rendering factory. Aborting ..."); } curFactory = factory; context.set(IRendererFactory.class, curFactory); IScopeContext[] contexts = new IScopeContext[] { DefaultScope.INSTANCE, InstanceScope.INSTANCE}; enableThemePreference = Platform.getPreferencesService().getBoolean("org.eclipse.e4.ui.workbench.renderers.swt", ENABLED_THEME_KEY, true, contexts); cssThemeChangedHandler = new StylingPreferencesHandler(context.get(Display.class)); } private static void populateModelInterfaces(MContext contextModel, IEclipseContext context, Class<?>[] interfaces) { for (Class<?> intf : interfaces) { if (Policy.DEBUG_CONTEXTS) { WorkbenchSWTActivator.trace(Policy.DEBUG_CONTEXTS_FLAG, "Adding " + intf.getName() + " for " //$NON-NLS-1$ //$NON-NLS-2$ + contextModel.getClass().getName(), null); } context.set(intf.getName(), contextModel); populateModelInterfaces(contextModel, context, intf.getInterfaces()); } } private String getContextName(MUIElement element) { StringBuilder builder = new StringBuilder(element.getClass() .getSimpleName()); String elementId = element.getElementId(); if (elementId != null && elementId.length() != 0) { builder.append(" (").append(elementId).append(") "); } builder.append("Context"); return builder.toString(); } @Override public Object createGui(final MUIElement element, final Object parentWidget, final IEclipseContext parentContext) { final Object[] gui = { null }; // wrap the handling in a SafeRunner so that exceptions do not prevent // the renderer from processing other elements SafeRunner.run(new ISafeRunnable() { @Override public void handleException(Throwable e) { if (e instanceof Error) { // errors are deadly, we shouldn't ignore these throw (Error) e; } // log exceptions otherwise if (logger != null) { String message = "Exception occurred while rendering: {0}"; //$NON-NLS-1$ logger.error(e, NLS.bind(message, element)); } } @Override public void run() throws Exception { gui[0] = safeCreateGui(element, parentWidget, parentContext); } }); return gui[0]; } public Object safeCreateGui(MUIElement element, Object parentWidget, IEclipseContext parentContext) { if (!element.isToBeRendered()) return null; // no creates while processing a remove if (removeRoot != null) { return null; } Object currentWidget = element.getWidget(); if (currentWidget != null) { if (currentWidget instanceof Control) { Control control = (Control) currentWidget; // make sure the control is visible MUIElement elementParent = element.getParent(); if (!(element instanceof MPlaceholder) || !(elementParent instanceof MPartStack)) control.setVisible(true); if (parentWidget instanceof Composite) { Composite currentParent = control.getParent(); if (currentParent != parentWidget) { // check if the original parent was a tab folder if (currentParent instanceof CTabFolder) { CTabFolder folder = (CTabFolder) currentParent; // if we used to be the tab folder's top right // control, unset it if (folder.getTopRight() == control) { folder.setTopRight(null); } } // the parents are different so we should reparent it control.setParent((Composite) parentWidget); } } } // Reparent the context (or the kid's context) if (element instanceof MContext) { IEclipseContext ctxt = ((MContext) element).getContext(); if (ctxt != null) ctxt.setParent(parentContext); } else { List<MContext> childContexts = modelService.findElements( element, null, MContext.class, null); for (MContext c : childContexts) { // Ensure that we only reset the context of our direct // children MUIElement kid = (MUIElement) c; MUIElement parent = kid.getParent(); if (parent == null && kid.getCurSharedRef() != null) parent = kid.getCurSharedRef().getParent(); if (!(element instanceof MPlaceholder) && parent != element) continue; if (c.getContext() != null && c.getContext().getParent() != parentContext) { c.getContext().setParent(parentContext); } } } // Now that we have a widget let the parent (if any) know MElementContainer<MUIElement> parentElement = element.getParent(); if (parentElement != null) { AbstractPartRenderer parentRenderer = getRendererFor(parentElement); if (parentRenderer != null) { parentRenderer.childRendered(parentElement, element); } } return element.getWidget(); } if (element instanceof MContext) { MContext ctxt = (MContext) element; // Assert.isTrue(ctxt.getContext() == null, // "Before rendering Context should be null"); if (ctxt.getContext() == null) { IEclipseContext lclContext = parentContext .createChild(getContextName(element)); populateModelInterfaces(ctxt, lclContext, element.getClass() .getInterfaces()); ctxt.setContext(lclContext); // System.out.println("New Context: " + lclContext.toString() // + " parent: " + parentContext.toString()); // make sure the context knows about these variables that have // been defined in the model for (String variable : ctxt.getVariables()) { lclContext.declareModifiable(variable); } Map<String, String> props = ctxt.getProperties(); for (Entry<String, String> entry : props.entrySet()) { lclContext.set(entry.getKey(), entry.getValue()); } } } // We check the widget again since it could be created by some UI event. // See Bug 417399 if (element.getWidget() != null) { return safeCreateGui(element, parentWidget, parentContext); } // Create a control appropriate to the part Object newWidget = createWidget(element, parentWidget); // Remember that we've created the control if (newWidget != null) { AbstractPartRenderer renderer = getRendererFor(element); // Have the renderer hook up any widget specific listeners renderer.hookControllerLogic(element); // Process its internal structure through the renderer that created // it if (element instanceof MElementContainer) { @SuppressWarnings("unchecked") MElementContainer<MUIElement> container = (MElementContainer<MUIElement>) element; renderer.processContents(container); } // Allow a final chance to set up renderer.postProcess(element); // Now that we have a widget let the parent (if any) know MElementContainer<MUIElement> parentElement = element.getParent(); if (parentElement != null) { AbstractPartRenderer parentRenderer = getRendererFor(parentElement); if (parentRenderer != null) { parentRenderer.childRendered(parentElement, element); } } } else { // failed to create the widget, dispose its context if necessary if (element instanceof MContext) { MContext ctxt = (MContext) element; IEclipseContext lclContext = ctxt.getContext(); if (lclContext != null) { lclContext.dispose(); ctxt.setContext(null); } } } return newWidget; } private IEclipseContext getContext(MUIElement parent) { if (parent instanceof MContext) { return ((MContext) parent).getContext(); } return modelService.getContainingContext(parent); } @Override public Object createGui(final MUIElement element) { final Object[] gui = { null }; // wrap the handling in a SafeRunner so that exceptions do not prevent // the renderer from processing other elements SafeRunner.run(new ISafeRunnable() { @Override public void handleException(Throwable e) { if (e instanceof Error) { // errors are deadly, we shouldn't ignore these throw (Error) e; } // log exceptions otherwise if (logger != null) { String message = "Exception occurred while rendering: {0}"; //$NON-NLS-1$ logger.error(e, NLS.bind(message, element)); } } @Override public void run() throws Exception { gui[0] = safeCreateGui(element); } }); return gui[0]; } private Object safeCreateGui(MUIElement element) { // Obtain the necessary parent widget Object parent = null; MUIElement parentME = element.getParent(); if (parentME == null) parentME = (MUIElement) ((EObject) element).eContainer(); if (parentME != null) { AbstractPartRenderer renderer = getRendererFor(parentME); if (renderer != null) { if (!element.isVisible()) { parent = getLimboShell(); } else { parent = renderer.getUIContainer(element); } } } // Obtain the necessary parent context IEclipseContext parentContext = null; if (element.getCurSharedRef() != null) { MPlaceholder ph = element.getCurSharedRef(); parentContext = getContext(ph.getParent()); } else if (parentContext == null && element.getParent() != null) { parentContext = getContext(element.getParent()); } else if (parentContext == null && element.getParent() == null) { parentContext = getContext((MUIElement) ((EObject) element) .eContainer()); } return safeCreateGui(element, parent, parentContext); } @Override public void focusGui(MUIElement element) { AbstractPartRenderer renderer = (AbstractPartRenderer) element .getRenderer(); if (renderer == null || element.getWidget() == null) return; Object implementation = element instanceof MContribution ? ((MContribution) element) .getObject() : null; // If there is no class to call @Focus on then revert to the default if (implementation == null) { renderer.forceFocus(element); return; } try { IEclipseContext context = getContext(element); Object defaultValue = new Object(); Object returnValue = ContextInjectionFactory.invoke(implementation, Focus.class, context, defaultValue); if (returnValue == defaultValue) { // No @Focus method, force the focus renderer.forceFocus(element); } } catch (InjectionException e) { log("Failed to grant focus to element", "Failed to grant focus to element ({0})", //$NON-NLS-1$ //$NON-NLS-2$ element.getElementId(), e); } catch (RuntimeException e) { log("Failed to grant focus to element via DI", //$NON-NLS-1$ "Failed to grant focus via DI to element ({0})", element.getElementId(), e); //$NON-NLS-1$ } } private void log(String unidentifiedMessage, String identifiedMessage, String id, Exception e) { if (id == null || id.length() == 0) { logger.error(e, unidentifiedMessage); } else { logger.error(e, NLS.bind(identifiedMessage, id)); } } private Shell getLimboShell() { if (limbo == null) { limbo = new Shell(Display.getCurrent(), SWT.NONE); limbo.setText("PartRenderingEngine's limbo"); //$NON-NLS-1$ // just for debugging, not shown anywhere // Place the limbo shell 'off screen' limbo.setLocation(0, 10000); limbo.setBackgroundMode(SWT.INHERIT_DEFAULT); limbo.setData(ShellActivationListener.DIALOG_IGNORE_KEY, Boolean.TRUE); limbo.addShellListener(new ShellAdapter() { @Override public void shellClosed(ShellEvent e) { // please don't close the limbo shell e.doit = false; } }); } return limbo; } /** * @param element */ @Override public void removeGui(final MUIElement element) { // wrap the handling in a SafeRunner so that exceptions do not prevent // the menu from being shown SafeRunner.run(new ISafeRunnable() { @Override public void handleException(Throwable e) { if (e instanceof Error) { // errors are deadly, we shouldn't ignore these throw (Error) e; } // log exceptions otherwise if (logger != null) { String message = "Exception occurred while unrendering: {0}"; //$NON-NLS-1$ logger.error(e, NLS.bind(message, element)); } } @Override public void run() throws Exception { safeRemoveGui(element); } }); } private void safeRemoveGui(MUIElement element) { if (removeRoot == null) removeRoot = element; // We call 'hideChild' *before* checking if the actual element // has been rendered in order to pick up cases of 'lazy loading' MUIElement parent = element.getParent(); AbstractPartRenderer parentRenderer = parent != null ? getRendererFor(parent) : null; if (parentRenderer != null) { parentRenderer.hideChild(element.getParent(), element); } AbstractPartRenderer renderer = getRendererFor(element); // If the element hasn't been rendered then this is a NO-OP if (renderer != null) { if (element instanceof MElementContainer<?>) { @SuppressWarnings("unchecked") MElementContainer<MUIElement> container = (MElementContainer<MUIElement>) element; MUIElement selectedElement = container.getSelectedElement(); List<MUIElement> children = container.getChildren(); // Bug 458460: Operate on a copy in case child nulls out parent for (MUIElement child : new ArrayList<>(children)) { // remove stuff in the "back" first if (child != selectedElement) { removeGui(child); } } if (selectedElement != null && children.contains(selectedElement)) { // now remove the selected element removeGui(selectedElement); } } if (element instanceof MPerspective) { MPerspective perspective = (MPerspective) element; for (MWindow subWindow : perspective.getWindows()) { removeGui(subWindow); } } else if (element instanceof MWindow) { MWindow window = (MWindow) element; for (MWindow subWindow : window.getWindows()) { removeGui(subWindow); } if (window instanceof MTrimmedWindow) { MTrimmedWindow trimmedWindow = (MTrimmedWindow) window; for (MUIElement trimBar : trimmedWindow.getTrimBars()) { removeGui(trimBar); } } } if (element instanceof MContribution) { MContribution contribution = (MContribution) element; Object client = contribution.getObject(); IEclipseContext parentContext = renderer.getContext(element); if (parentContext != null && client != null) { try { ContextInjectionFactory.invoke(client, PersistState.class, parentContext, null); } catch (Exception e) { if (logger != null) { logger.error(e); } } } } renderer.disposeWidget(element); // unset the client object if (element instanceof MContribution) { MContribution contribution = (MContribution) element; Object client = contribution.getObject(); IEclipseContext parentContext = renderer.getContext(element); if (parentContext != null && client != null) { try { ContextInjectionFactory.uninject(client, parentContext); } catch (Exception e) { if (logger != null) { logger.error(e); } } } contribution.setObject(null); } // dispose the context if (element instanceof MContext) { clearContext((MContext) element); } } if (element instanceof MPlaceholder) { MPlaceholder ph = (MPlaceholder) element; if (ph.getRef() != null && ph.getRef().getCurSharedRef() == ph) { ph.getRef().setCurSharedRef(null); } } if (removeRoot == element) removeRoot = null; } private void clearContext(MContext contextME) { MContext ctxt = contextME; IEclipseContext lclContext = ctxt.getContext(); if (lclContext != null) { IEclipseContext parentContext = lclContext.getParent(); IEclipseContext child = parentContext != null ? parentContext .getActiveChild() : null; if (child == lclContext) { child.deactivate(); } ctxt.setContext(null); lclContext.dispose(); } } protected Object createWidget(MUIElement element, Object parent) { AbstractPartRenderer renderer = getRenderer(element, parent); if (renderer != null) { // Remember which renderer is responsible for this widget element.setRenderer(renderer); Object newWidget = renderer.createWidget(element, parent); if (newWidget != null) { renderer.bindWidget(element, newWidget); return newWidget; } } return null; } private AbstractPartRenderer getRenderer(MUIElement uiElement, Object parent) { // Is there a custom renderer defined ? String customURI = uiElement.getPersistedState().get(IPresentationEngine.CUSTOM_RENDERER_KEY); if (customURI != null) { AbstractPartRenderer abstractPartRenderer = customRendererMap.get(customURI); if (abstractPartRenderer != null) { return abstractPartRenderer; } IEclipseContext owningContext = modelService.getContainingContext(uiElement); IContributionFactory contributionFactory = owningContext.get(IContributionFactory.class); Object customRenderer = contributionFactory.create(customURI, owningContext); if (customRenderer instanceof AbstractPartRenderer) { customRendererMap.put(customURI, (AbstractPartRenderer) customRenderer); return (AbstractPartRenderer) customRenderer; } } // If not then use the default renderer return curFactory.getRenderer(uiElement, parent); } protected AbstractPartRenderer getRendererFor(MUIElement element) { return (AbstractPartRenderer) element.getRenderer(); } @Override @Inject @Optional public Object run(final MApplicationElement uiRoot, final IEclipseContext runContext) { final Display display; if (runContext.get(Display.class) != null) { display = runContext.get(Display.class); } else { display = Display.getDefault(); runContext.set(Display.class, display); } Realm.runWithDefault(DisplayRealm.getRealm(display), new Runnable() { @Override public void run() { initializeStyling(display, runContext); // Register an SWT resource handler runContext.set(IResourceUtilities.class, new ResourceUtility()); // set up the keybinding manager KeyBindingDispatcher dispatcher = ContextInjectionFactory.make(KeyBindingDispatcher.class, runContext); runContext.set(KeyBindingDispatcher.class, dispatcher); keyListener = dispatcher.getKeyDownFilter(); display.addFilter(SWT.KeyDown, keyListener); display.addFilter(SWT.Traverse, keyListener); // Show the initial UI // Create a 'limbo' shell (used to host controls that shouldn't // be in the current layout) Shell limbo = getLimboShell(); runContext.set("limbo", limbo); // HACK!! we should loop until the display gets disposed... // ...then we listen for the last 'main' window to get disposed // and dispose the Display testShell = null; theApp = null; boolean spinOnce = true; if (uiRoot instanceof MApplication) { ShellActivationListener shellDialogListener = new ShellActivationListener((MApplication) uiRoot); display.addFilter(SWT.Activate, shellDialogListener); display.addFilter(SWT.Deactivate, shellDialogListener); spinOnce = false; // loop until the app closes theApp = (MApplication) uiRoot; // long startTime = System.currentTimeMillis(); for (MWindow window : theApp.getChildren()) { createGui(window); } // long endTime = System.currentTimeMillis(); // System.out.println("Render: " + (endTime - startTime)); // tell the app context we are starting so the splash is // torn down IApplicationContext ac = appContext.get(IApplicationContext.class); if (ac != null) { ac.applicationRunning(); if (eventBroker != null) { eventBroker.post( UIEvents.UILifeCycle.APP_STARTUP_COMPLETE, theApp); } } } else if (uiRoot instanceof MUIElement) { if (uiRoot instanceof MWindow) { testShell = (Shell) createGui((MUIElement) uiRoot); } else { // Special handling for partial models (for testing...) testShell = new Shell(display, SWT.SHELL_TRIM); createGui((MUIElement) uiRoot, testShell, null); } } // allow any early startup extensions to run Runnable earlyStartup = (Runnable) runContext.get(EARLY_STARTUP_HOOK); if (earlyStartup != null) { earlyStartup.run(); } TestableObject testableObject = runContext.get(TestableObject.class); if (testableObject instanceof E4Testable) { ((E4Testable) testableObject).init(display, runContext.get(IWorkbench.class)); } IEventLoopAdvisor advisor = runContext.getActiveLeaf().get(IEventLoopAdvisor.class); if (advisor == null) { advisor = new IEventLoopAdvisor() { @Override public void eventLoopIdle(Display display) { display.sleep(); } @Override public void eventLoopException(Throwable exception) { StatusReporter statusReporter = appContext.get(StatusReporter.class); if (statusReporter != null) { statusReporter.show(StatusReporter.ERROR, "Internal Error", exception); } else { if (logger != null) { logger.error(exception); } } } }; } final IEventLoopAdvisor finalAdvisor = advisor; display.setErrorHandler(e -> { // If e is one of the exception types that are generally // recoverable, hand it to the event loop advisor if (e instanceof LinkageError || e instanceof AssertionError) { handle(e, finalAdvisor); } else { // Otherwise, rethrow it throw e; } }); display.setRuntimeExceptionHandler(e -> handle(e, finalAdvisor)); // Spin the event loop until someone disposes the display while (((testShell != null && !testShell.isDisposed()) || (theApp != null && someAreVisible(theApp .getChildren()))) && !display.isDisposed()) { try { if (!display.readAndDispatch()) { runContext.processWaiting(); if (spinOnce) { return; } advisor.eventLoopIdle(display); } } catch (ThreadDeath th) { throw th; } catch (Exception ex) { handle(ex, advisor); } catch (Error err) { handle(err, advisor); } } if (!spinOnce) { cleanUp(); } } private void handle(Throwable ex, IEventLoopAdvisor advisor) { try { advisor.eventLoopException(ex); } catch (Throwable t) { if (t instanceof ThreadDeath) { throw (ThreadDeath) t; } // couldn't handle the exception, print to console t.printStackTrace(); } } }); return IApplication.EXIT_OK; } protected boolean someAreVisible(List<MWindow> windows) { // This method is called from the event dispatch loop, so the // following optimization is in order... // Ideally, we'd just do: // for (MWindow win : theApp.getChildren()) { // But this creates an iterator (which must be GC'd) // at every call. The code below creates no objects. final int limit = windows.size(); for (int i = 0; i < limit; i++) { final MWindow win = windows.get(i); // Note: Removed isVisible test, as this should have // no impact on the whether the event loop // terminates - non-visible windows still exists // and can receive events. // Note: isToBeRendered() == true => win.getWidget() != null // but I'm not sure whether there is latency between setting // toBeRendered and the creation of the widget. So, keeping // both tests seems prudent. if (win.isToBeRendered() && win.getWidget() != null) { return true; } } return false; } @Override public void stop() { // FIXME Without this call the test-suite fails cleanUp(); if (theApp != null) { for (MWindow window : theApp.getChildren()) { if (window.getWidget() != null) { removeGui(window); } } } else if (testShell != null && !testShell.isDisposed()) { Object model = testShell.getData(AbstractPartRenderer.OWNING_ME); if (model instanceof MUIElement) { removeGui((MUIElement) model); } else { testShell.close(); } } } /* * There are situations where this is called more than once until we know * why this is needed we should make this safe for multiple calls */ private void cleanUp() { if (keyListener != null) { Display display = Display.getDefault(); if (!display.isDisposed()) { display.removeFilter(SWT.KeyDown, keyListener); display.removeFilter(SWT.Traverse, keyListener); keyListener = null; } } } public static void initializeStyling(Display display, IEclipseContext appContext) { String cssTheme = (String) appContext.get(E4Application.THEME_ID); String cssURI = (String) appContext.get(IWorkbench.CSS_URI_ARG); if ("none".equals(cssTheme) || (!enableThemePreference)) { appContext.set(IStylingEngine.SERVICE_NAME, new IStylingEngine() { @Override public void setClassname(Object widget, String classname) { WidgetElement.setCSSClass((Widget) widget, classname); } @Override public void setId(Object widget, String id) { WidgetElement.setID((Widget) widget, id); } @Override public void style(Object widget) { } @Override public CSSStyleDeclaration getStyle(Object widget) { return null; } @Override public void setClassnameAndId(Object widget, String classname, String id) { WidgetElement.setCSSClass((Widget) widget, classname); WidgetElement.setID((Widget) widget, id); } }); } else if (cssTheme != null) { final IThemeEngine themeEngine = createThemeEngine(display, appContext); String cssResourcesURI = (String) appContext.get(IWorkbench.CSS_RESOURCE_URI_ARG); // Create the OSGi resource locator if (cssResourcesURI != null) { // TODO: Should this be set through an extension as well? themeEngine.registerResourceLocator(new OSGiResourceLocator(cssResourcesURI)); } appContext.set(IStylingEngine.SERVICE_NAME, new IStylingEngine() { @Override public void setClassname(Object widget, String classname) { WidgetElement.setCSSClass((Widget) widget, classname); themeEngine.applyStyles(widget, true); } @Override public void setId(Object widget, String id) { WidgetElement.setID((Widget) widget, id); themeEngine.applyStyles(widget, true); } @Override public void style(Object widget) { themeEngine.applyStyles(widget, true); } @Override public CSSStyleDeclaration getStyle(Object widget) { return themeEngine.getStyle(widget); } @Override public void setClassnameAndId(Object widget, String classname, String id) { WidgetElement.setCSSClass((Widget) widget, classname); WidgetElement.setID((Widget) widget, id); themeEngine.applyStyles(widget, true); } }); setCSSTheme(display, themeEngine, cssTheme); } else if (cssURI != null) { String cssResourcesURI = (String) appContext.get(IWorkbench.CSS_RESOURCE_URI_ARG); final CSSSWTEngineImpl cssEngine = new CSSSWTEngineImpl(display, true); WidgetElement.setEngine(display, cssEngine); if (cssResourcesURI != null) { cssEngine.getResourcesLocatorManager().registerResourceLocator( new OSGiResourceLocator(cssResourcesURI.toString())); } // FIXME: is this needed? display.setData("org.eclipse.e4.ui.css.context", appContext); //$NON-NLS-1$ appContext.set(IStylingEngine.SERVICE_NAME, new IStylingEngine() { @Override public void setClassname(Object widget, String classname) { WidgetElement.setCSSClass((Widget) widget, classname); cssEngine.applyStyles(widget, true); } @Override public void setId(Object widget, String id) { WidgetElement.setID((Widget) widget, id); cssEngine.applyStyles(widget, true); } @Override public void style(Object widget) { cssEngine.applyStyles(widget, true); } @Override public CSSStyleDeclaration getStyle(Object widget) { Element e = cssEngine.getCSSElementContext(widget).getElement(); if (e == null) { return null; } return cssEngine.getViewCSS().getComputedStyle(e, null); } @Override public void setClassnameAndId(Object widget, String classname, String id) { WidgetElement.setCSSClass((Widget) widget, classname); WidgetElement.setID((Widget) widget, id); cssEngine.applyStyles(widget, true); } }); URL url; try { url = FileLocator.resolve(new URL(cssURI)); try (InputStream stream = url.openStream()) { cssEngine.parseStyleSheet(stream); } } catch (IOException e) { Activator.log(LogService.LOG_ERROR, e.getMessage(), e); } Shell[] shells = display.getShells(); for (Shell s : shells) { try { s.setRedraw(false); s.reskin(SWT.ALL); cssEngine.applyStyles(s, true); } catch (Exception e) { Activator.log(LogService.LOG_ERROR, e.getMessage(), e); } finally { s.setRedraw(true); } } } CSSRenderingUtils cssUtils = ContextInjectionFactory.make(CSSRenderingUtils.class, appContext); appContext.set(CSSRenderingUtils.class, cssUtils); } private static IThemeEngine createThemeEngine(Display display, IEclipseContext appContext) { // Store the app context IContributionFactory contribution = appContext.get(IContributionFactory.class); IEclipseContext cssContext = EclipseContextFactory.create(); cssContext.set(IContributionFactory.class, contribution); display.setData("org.eclipse.e4.ui.css.context", cssContext); //$NON-NLS-1$ IThemeManager mgr = appContext.get(IThemeManager.class); IThemeEngine themeEngine = mgr.getEngineForDisplay(display); appContext.set(IThemeEngine.class, themeEngine); return themeEngine; } private static void setCSSTheme(Display display, IThemeEngine themeEngine, String cssTheme) { if (display.getHighContrast()) { themeEngine.setTheme(cssTheme, false); } else { themeEngine.restore(cssTheme); } } public static class StylingPreferencesHandler implements EventHandler { private HashSet<IEclipsePreferences> prefs = null; public StylingPreferencesHandler(Display display) { if (display != null) { display.addListener(SWT.Dispose, createOnDisplayDisposedListener()); } } protected Listener createOnDisplayDisposedListener() { return event -> resetOverriddenPreferences(); } @Override public void handleEvent(Event event) { resetOverriddenPreferences(); overridePreferences(getThemeEngine(event)); } protected void resetOverriddenPreferences() { for (IEclipsePreferences preferences : getPreferences()) { resetOverriddenPreferences(preferences); } } protected void resetOverriddenPreferences(IEclipsePreferences preferences) { for (String name : getOverriddenPropertyNames(preferences)) { preferences.remove(name); } removeOverriddenPropertyNames(preferences); } protected void removeOverriddenPropertyNames(IEclipsePreferences preferences) { EclipsePreferencesHelper.removeOverriddenPropertyNames(preferences); } protected List<String> getOverriddenPropertyNames(IEclipsePreferences preferences) { return EclipsePreferencesHelper.getOverriddenPropertyNames(preferences); } protected Set<IEclipsePreferences> getPreferences() { if (prefs == null) { prefs = new HashSet<>(); BundleContext context = WorkbenchSWTActivator.getDefault().getContext(); for (Bundle bundle : context.getBundles()) { if (bundle.getSymbolicName() != null) { prefs.add(InstanceScope.INSTANCE.getNode(bundle.getSymbolicName())); } } } return prefs; } private void overridePreferences(IThemeEngine themeEngine) { if (themeEngine != null) { for (IEclipsePreferences preferences : getPreferences()) { themeEngine.applyStyles(preferences, false); } } } private IThemeEngine getThemeEngine(Event event) { return (IThemeEngine) event.getProperty(IThemeEngine.Events.THEME_ENGINE); } } }