blob: 3152b49e0d130cee87d0d7fd4646c50496c9e86f [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014 BestSolution.at 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:
* Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation
*******************************************************************************/
package org.eclipse.fx.ui.workbench.fx;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.e4.core.contexts.ContextInjectionFactory;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.ui.internal.workbench.E4Workbench;
import org.eclipse.e4.ui.workbench.IResourceUtilities;
import org.eclipse.e4.ui.workbench.IWorkbench;
import org.eclipse.e4.ui.workbench.lifecycle.PreSave;
import org.eclipse.emf.common.util.URI;
import org.eclipse.equinox.app.IApplication;
import org.eclipse.equinox.app.IApplicationContext;
import org.eclipse.fx.core.SystemUtils;
import org.eclipse.fx.core.app.ApplicationContext;
import org.eclipse.fx.core.app.ApplicationContext.Splash;
import org.eclipse.fx.core.app.ApplicationInstance;
import org.eclipse.fx.core.app.ApplicationLocation;
import org.eclipse.fx.core.app.ExitStatus;
import org.eclipse.fx.core.databinding.JFXRealm;
import org.eclipse.fx.core.log.LoggerCreator;
import org.eclipse.fx.osgi.util.AbstractJFXApplication;
import org.eclipse.fx.ui.services.Constants;
import org.eclipse.fx.ui.services.resources.GraphicsLoader;
import org.eclipse.fx.ui.services.startup.StartupProgressTrackerService;
import org.eclipse.fx.ui.services.startup.StartupProgressTrackerService.DefaultProgressState;
import org.eclipse.fx.ui.services.startup.StartupProgressTrackerService.ProgressState;
import org.eclipse.fx.ui.services.sync.UISynchronize;
import org.eclipse.fx.ui.services.theme.Theme;
import org.eclipse.fx.ui.services.theme.ThemeManager;
import org.eclipse.fx.ui.workbench.base.AbstractE4Application;
import org.eclipse.fx.ui.workbench.fx.internal.GraphicsLoaderImpl;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.osgi.service.datalocation.Location;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.osgi.service.prefs.BackingStoreException;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.image.Image;
import javafx.stage.Screen;
import javafx.stage.Stage;
/**
* default e4 application
*/
@SuppressWarnings("restriction")
public class E4Application extends AbstractE4Application implements IApplication, ApplicationInstance {
static org.eclipse.fx.core.log.Logger LOGGER = LoggerCreator.createLogger(E4Application.class);
E4Workbench workbench;
ApplicationLocation instanceLocation;
IEclipseContext workbenchContext;
private static final String PRIMARY_STAGE_KEY = "primaryStage"; //$NON-NLS-1$
private static final String CREATE_WORKBENCH_ON_NON_UI_THREAD_ARG = "createWorkbenchOnNonUIThread"; //$NON-NLS-1$
static E4Application SELF;
ApplicationContext applicationContext;
ExitStatus returnValue;
protected EventAdmin eventAdmin;
StartupProgressTrackerService startupService;
/**
* Gets the {@link E4Application}.
* <p>
* The {@link E4Application} is not available prior to calling the
* {@link #start(IApplicationContext)} by the equinox's launcher.
*
* @return the instance of the {@link E4Application}.
*/
public static @NonNull E4Application getE4Application() {
E4Application self = SELF;
if( self == null ) {
throw new IllegalStateException("Application not yet initialized"); //$NON-NLS-1$
}
return self;
}
@Override
public CompletableFuture<ExitStatus> launch(ApplicationContext context) {
SELF = this;
this.applicationContext = context;
Bundle b = FrameworkUtil.getBundle(AbstractJFXApplication.class);
BundleContext bundleContext = b.getBundleContext();
ServiceReference<EventAdmin> ref = bundleContext.getServiceReference(EventAdmin.class);
if (ref != null) {
this.eventAdmin = bundleContext.getService(ref);
}
ServiceReference<StartupProgressTrackerService> serviceReference = bundleContext.getServiceReference(StartupProgressTrackerService.class);
if( serviceReference != null ) {
this.startupService = bundleContext.getService(serviceReference);
this.startupService.applicationLaunched(this.applicationContext);
} else {
// if the service is not available we make sure to bring the splash down
this.applicationContext.applicationRunning();
}
CompletableFuture<ExitStatus> rv = CompletableFuture.supplyAsync( () -> {
try {
launchE4JavaFxApplication();
} catch (Exception e) {
if( e instanceof RuntimeException ) {
throw (RuntimeException)e;
} else {
throw new RuntimeException(e);
}
}
try {
return this.returnValue == null ? ExitStatus.OK : this.returnValue;
} finally {
this.returnValue = null;
}
},Executors.newSingleThreadExecutor());
return rv;
}
@Override
public Object start(IApplicationContext context) throws Exception {
SELF = this;
this.applicationContext = new ApplicationContextImpl(context);
Bundle b = FrameworkUtil.getBundle(AbstractJFXApplication.class);
BundleContext bundleContext = b.getBundleContext();
ServiceReference<EventAdmin> ref = bundleContext.getServiceReference(EventAdmin.class);
if (ref != null) {
this.eventAdmin = bundleContext.getService(ref);
}
ServiceReference<StartupProgressTrackerService> serviceReference = bundleContext.getServiceReference(StartupProgressTrackerService.class);
if( serviceReference != null ) {
this.startupService = bundleContext.getService(serviceReference);
this.startupService.applicationLaunched(this.applicationContext);
} else {
// if the service is not available we make sure to bring the splash down
this.applicationContext.applicationRunning();
}
launchE4JavaFxApplication();
if( this.returnValue == null ) {
return IApplication.EXIT_OK;
} else {
if( this.returnValue == ExitStatus.OK ) {
return IApplication.EXIT_OK;
} else {
return IApplication.EXIT_RESTART;
}
}
}
/**
* Binds the given {@link Application} to the {@link E4Workbench} and
* launches the application.
* <p>
* If the {@link E4Workbench} has not been initialized yet then the
* {@link #initE4Workbench(IApplicationContext, Application, Stage)
* initE4Workbench} method will be called before launching the application.
*
* @param context
* the {@link IApplicationContext}.
* @param jfxApplication
* the {@link Application}.
* @param primaryStage
* the primary stage associated with the given
* {@link Application}.
*/
public void jfxStart(ApplicationContext context, Application jfxApplication, Stage primaryStage) {
updateStartupState(DefaultProgressState.JAVAFX_INITIALIZED);
Runnable startRunnable = new Runnable() {
@Override
public void run() {
if (E4Application.this.workbench == null) {
if(! initE4Workbench(context, jfxApplication, primaryStage) ) {
calculateReturnValue();
return;
}
}
E4Workbench workbench = E4Application.this.workbench;
if( workbench == null ) {
throw new IllegalStateException("Not workbench instance yet available"); //$NON-NLS-1$
}
IEclipseContext wbContext = workbench.getContext();
if( wbContext == null ) {
throw new IllegalStateException("The workbench has no context assigned"); //$NON-NLS-1$
}
UISynchronize uiSync = workbench.getContext().get(UISynchronize.class);
uiSync.syncExec(() -> {
E4Application.this.instanceLocation = (ApplicationLocation) wbContext.get(E4Workbench.INSTANCE_LOCATION);
try {
if (!checkInstanceLocation(E4Application.this.instanceLocation, wbContext)) {
updateStartupState(DefaultProgressState.LOCATION_CHECK_FAILED);
return;
}
// Create and run the UI (if any)
workbench.createAndRunUI(E4Application.this.workbench.getApplication());
} finally {
postJfxStarted(context);
}
});
}
};
if (getArgValue(CREATE_WORKBENCH_ON_NON_UI_THREAD_ARG, context, true) != null) {
Thread t = new Thread(startRunnable);
t.start();
} else {
startRunnable.run();
}
}
@Override
protected void preLifecycle(IEclipseContext appContext) {
super.preLifecycle(appContext);
appContext.set(GraphicsLoader.class, ContextInjectionFactory.make(GraphicsLoaderImpl.class, appContext));
}
/**
* Stops the application.
*/
public void jfxStop() {
calculateReturnValue();
try {
if (this.workbenchContext != null && this.workbench != null) {
// Save the model into the targetURI
if (getLifecycleManager() != null) {
ContextInjectionFactory.invoke(getLifecycleManager(), PreSave.class, this.workbenchContext, null);
}
ThemeManager manager = this.workbenchContext.get(ThemeManager.class);
Theme theme = manager.getCurrentTheme();
if( theme != null ) {
IEclipsePreferences node = InstanceScope.INSTANCE.getNode("org.eclipse.fx.ui.workbench.fx"); //$NON-NLS-1$
node.put(AbstractE4Application.THEME_ID, theme.getId());
try {
node.flush();
} catch (BackingStoreException e) {
LOGGER.error("Failed to remember the theme id", e); //$NON-NLS-1$
}
}
this.workbench.close();
// save after the shutdown see Bug 446933
saveModel();
}
} finally {
if (this.instanceLocation != null)
this.instanceLocation.release();
}
}
void calculateReturnValue() {
Object result = null;
if (this.workbenchContext != null) {
result = this.workbenchContext.get(EXIT_CODE);
if (result == null && this.workbench != null && this.workbench.isRestart()) {
result = ExitStatus.RESTART;
}
}
if (result != null && result instanceof ExitStatus) {
this.returnValue = (ExitStatus) result;
} else if( this.returnValue == null ) {
this.returnValue = ExitStatus.OK;
}
}
@Override
public void stop(ExitStatus status) {
this.returnValue = status;
}
@Override
public void stop() {
// do nothing
}
/**
* Initializes the {@link E4Workbench}.
* <p>
* By calling this method the {@link E4Workbench} is initialized but the UI
* is not displayed yet. By combining calling this method and the
* {@link #jfxStart(IApplicationContext, Application, Stage) jfxStart}
* method the developer can create a custom initializer which might do
* additional actions between creating {@link IWorkbench} and showing the
* UI.
*
* @param context
* the {@link IApplicationContext}.
* @param jfxApplication
* the application which is going to be launched.
* @param primaryStage
* the primary stage.
* @return <code>true</code> if the workbench was initialized successfully
*/
public boolean initE4Workbench(final ApplicationContext context, Application jfxApplication, final Stage primaryStage) {
this.workbenchContext = createApplicationContext();
// It is the very first time when the javaFX Application appears. It
// will be used to render the UI.
// Add following objects to the context since they might be needed by
// life-cycle manager and
// @PostContextCreate implementations.
this.workbenchContext.set(Application.class, jfxApplication);
this.workbenchContext.set(ApplicationContext.class, context);
this.workbenchContext.set(PRIMARY_STAGE_KEY, primaryStage);
this.workbench = createE4Workbench(context, this.workbenchContext);
return this.workbench != null;
}
/**
* @return the {@link IApplicationContext}.
*/
public ApplicationContext getApplicationContext() {
return this.applicationContext;
}
/**
* Launches the JavaFX application by calling the
* {@link Application#launch(Class, String...)} method.
* <p>
* <b>NOTE</b>: this method is intended to be overridden by subclasses of
* the {@link E4Application} that would like provide a custom application
* launcher.
*
* @throws Exception
* when the application could not be started.
*/
protected void launchE4JavaFxApplication() throws Exception {
Application.launch(getJfxApplicationClass());
}
/**
* Gets the class which should be launched. It must be a subclass of the
* JavaFX's {@link Application}.
* <p>
* <b>NOTE</b>: this method is intended to be overridden by subclasses of
* the {@link E4Application} that would like to provide a custom
* implementation of the {@link Application} class.
*
* @return the Class which should be launched.
*/
@SuppressWarnings("static-method")
protected Class<? extends Application> getJfxApplicationClass() {
return DefaultJFXApp.class;
}
/**
* Performs additional actions just after staring the application.
*
* @param context
* the {@link IApplicationContext}.
*/
protected void postJfxStarted(final ApplicationContext context) {
final Map<String, Object> map = new HashMap<String, Object>();
sendEvent(Constants.APPLICATION_LAUNCHED, map);
updateStartupState(DefaultProgressState.WORKBENCH_GUI_SHOWING);
}
@Override
protected Realm createRealm(IEclipseContext appContext) {
return JFXRealm.createDefault();
}
/**
* Update the startup progress state
* @param progressState
*/
protected void updateStartupState(ProgressState progressState) {
if( this.startupService != null ) {
this.startupService.stateReached(progressState);
}
}
@Override
protected IResourceUtilities<Image> createResourceUtility(final IEclipseContext appContext) {
return new IResourceUtilities<Image>() {
@Override
public Image imageDescriptorFromURI(URI iconPath) {
if( iconPath == null ) {
return null;
}
GraphicsLoader l = appContext.get(GraphicsLoader.class);
return l.getImage(new EMFUri(iconPath));
}
};
}
@Override
protected String getDefaultPresentationEngineURI(IEclipseContext appContext) {
return PartRenderingEngine.engineURI;
}
/**
* Sends the event.
*
* @param topic
* the topic.
* @param map
* the payload data.
*/
protected void sendEvent(final String topic, final Map<String, Object> map) {
if (this.eventAdmin != null) {
this.eventAdmin.sendEvent(new Event(topic, map));
} else {
LOGGER.warningf("Could not send the %s event. EventAdmin is missing.", topic); //$NON-NLS-1$
}
}
static class ApplicationContextImpl implements ApplicationContext {
private final IApplicationContext application;
public ApplicationContextImpl(IApplicationContext application) {
this.application = application;
}
@Override
public void applicationRunning() {
this.application.applicationRunning();
}
@Override
public Optional<Splash> getSplashImage() {
Bundle bundle = this.application.getBrandingBundle();
if( bundle != null ) {
URL url = bundle.getResource("splash.bmp"); //$NON-NLS-1$
if( url != null ) {
return Optional.of(new SplashImpl(url));
}
}
return Optional.empty();
}
@Override
public String[] getApplicationArguments() {
return (String[]) this.application.getArguments().get(IApplicationContext.APPLICATION_ARGS);
}
@Override
public Object getApplicationProperty(String key) {
return this.application.getBrandingProperty(key);
}
@Override
public Optional<ApplicationLocation> getInstanceLocation() {
BundleContext context = FrameworkUtil.getBundle(getClass()).getBundleContext();
try {
Collection<ServiceReference<@NonNull Location>> references = context.getServiceReferences(Location.class, Location.INSTANCE_FILTER);
if( ! references.isEmpty() ) {
return Optional.of(new ApplicationLocationImpl(context.getService(references.iterator().next())));
} else {
LoggerCreator.createLogger(getClass()).error("Could not find an instance location service"); //$NON-NLS-1$
}
} catch (InvalidSyntaxException e) {
LoggerCreator.createLogger(getClass()).error("Failed to create a filter '"+Location.INSTANCE_FILTER+"'", e); //$NON-NLS-1$//$NON-NLS-2$
}
return Optional.empty();
}
}
static class ApplicationLocationImpl implements ApplicationLocation, Location {
private final Location location;
public ApplicationLocationImpl(Location location) {
this.location = location;
}
@Override
public boolean lock() throws IOException {
return this.location.lock();
}
@Override
public void release() {
this.location.release();
}
@Override
public Path getPath() {
URL url = this.location.getURL();
try {
return Paths.get(url.toURI());
} catch (URISyntaxException e) {
LoggerCreator.createLogger(getClass()).error("Failed to get the path for '"+url+"'", e); //$NON-NLS-1$//$NON-NLS-2$
}
return null;
}
@Override
public URL getURL() {
return this.location.getURL();
}
@Override
public boolean isReadOnly() {
return this.location.isReadOnly();
}
@Override
public boolean allowsDefault() {
return this.location.allowsDefault();
}
@Override
public URL getDefault() {
return this.location.getDefault();
}
@Override
public Location getParentLocation() {
return this.location.getParentLocation();
}
@Override
public boolean isSet() {
return this.location.isSet();
}
@Override
public boolean setURL(URL value, boolean lock) throws IllegalStateException {
return this.location.setURL(value, lock);
}
@Override
public boolean set(URL value, boolean lock) throws IllegalStateException, IOException {
return this.location.set(value, lock);
}
@Override
public boolean set(URL value, boolean lock, String lockFilePath) throws IllegalStateException, IOException {
return this.location.set(value, lock, lockFilePath);
}
@Override
public boolean isLocked() throws IOException {
return this.location.isLocked();
}
@Override
public Location createLocation(Location parent, URL defaultValue, boolean readonly) {
return this.location.createLocation(parent, defaultValue, readonly);
}
@Override
public URL getDataArea(String path) throws IOException {
return this.location.getDataArea(path);
}
}
static class SplashImpl implements Splash {
private final URL url;
private Point2D location;
public SplashImpl(URL url) {
this.url = url;
}
@Override
public URL getUrl() {
return this.url;
}
@Override
public Point2D getLocation() {
if( this.location == null ) {
Image img = new Image(getUrl().toExternalForm());
double x = SystemUtils.isMacOS() ? Screen.getPrimary().getVisualBounds().getWidth() / 2 - img.getWidth() / 2 - 1 : Screen.getPrimary().getBounds().getWidth() / 2 - img.getWidth() / 2 - 1;
double y = SystemUtils.isMacOS() ? 227 : Screen.getPrimary().getBounds().getHeight() / 2 - img.getHeight() / 2;
this.location = new Point2D(x, y);
}
return this.location;
}
}
}