/*******************************************************************************
* Copyright (c) 2007, 2014 compeople AG 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:
* compeople AG - initial API and implementation
*******************************************************************************/
package org.eclipse.riena.ui.swt.utils;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.osgi.service.log.LogService;
import org.eclipse.core.runtime.Assert;
import org.eclipse.equinox.log.Logger;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.ShellAdapter;
import org.eclipse.swt.events.ShellEvent;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbenchSite;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.riena.core.Log4r;
import org.eclipse.riena.core.util.ReflectionUtils;
import org.eclipse.riena.internal.ui.swt.Activator;
/**
* Manages one or more "detached" views.
* <p>
* Client code must {@link #dispose()} instances when no longer needed.
*/
public class DetachedViewsManager {
/**
* Name of the method that creates the controller for SubModuleViews.
*/
private static final String METHOD_CREATE_CONTROLLER = "createController"; //$NON-NLS-1$
private final static Logger LOGGER = Log4r.getLogger(Activator.getDefault(), DetachedViewsManager.class);
/**
* Maps a String ('id') to a shell instance holding a detached view.
*/
private final Map<String, Shell> id2shell;
/**
* The main shell; detached windows use it as their parent shell.
*/
private final Shell mainShell;
/**
* Create a new DetachedViewsManager instance.
* <p>
* Client code must {@link #dispose()} instances when no longer needed.
*
* @param site
* the workbench site; never null
* @deprecated This constructor will not work in e4. Use <tt>DetachedViewsManager(Shell s)</tt>
*/
@Deprecated
public DetachedViewsManager(final IWorkbenchSite site) {
this(site.getShell());
}
/**
* Create a new DetachedViewsManager instance.
* <p>
* Client code must {@link #dispose()} instances when no longer needed.
*
* @param shell
* the main shell; never null; detached windows will use this
* shell as their parent shell
*/
public DetachedViewsManager(final Shell shell) {
Assert.isNotNull(shell);
id2shell = new HashMap<String, Shell>();
this.mainShell = shell;
}
/**
* Close (=dispose) thew view / shell with the given id.
*
* @param id
* id of the view to show; must be unique within this instance;
* never null
*/
public void closeView(final String id) {
Assert.isNotNull(id);
final Shell shell = id2shell.remove(id);
if (shell != null) {
if (!shell.isDisposed()) {
shell.dispose();
}
}
}
/**
* Deallocate resources used by this class. Client code must
* {@link #dispose()} instances when no longer needed.
*/
public void dispose() {
final String[] keys = id2shell.keySet().toArray(new String[id2shell.size()]);
for (final String id : keys) {
closeView(id);
}
}
/**
* Return the shell hosting the view with the given id
*
* @param id
* the view / shell id; never null
* @return a Shell instance or null
*/
public Shell getShell(final String id) {
Assert.isNotNull(id);
return id2shell.get(id);
}
/**
* Hide the view / shell with the given id
*
* @param id
* id of the view / shell to hide; never null
*/
public void hideView(final String id) {
Assert.isNotNull(id);
final Shell shell = id2shell.get(id);
if (shell != null) {
shell.setVisible(false);
}
}
/**
* Show the view / shell with the given id. If the given id matches no
* shell, then a new shell / view is created using the given viewClazz.
*
* @param id
* id of the view / shell to show; must be unique within this
* instance; not tied to the view-part id, since the view is
* created by reflection; never null
* @param viewClazz
* the class of the view; must have parameterless constructor;
* never null
* @param position
* one of SWT.LEFT, SWT.RIGHT, SWT.TOP, SWT.BOTTOM. Will place
* the view to the specified edge of the main window. Note the
* position prefference is only applied if a new shell is
* created.
*/
public void showView(final String id, final Class<? extends ViewPart> viewClazz, final int position) {
Assert.isNotNull(id, "id"); //$NON-NLS-1$
Assert.isLegal(id.trim().length() > 0, String.format("id cannot be null or empty: '%s'", id)); //$NON-NLS-1$
Assert.isNotNull(viewClazz);
Shell shell = id2shell.get(id);
if (shell == null) {
int x;
int y;
Rectangle bounds;
final Rectangle viewBounds = this.mainShell.getBounds();
switch (position) {
case SWT.RIGHT:
x = viewBounds.x + viewBounds.width;
y = viewBounds.y;
bounds = new Rectangle(x, y, viewBounds.width / 2, viewBounds.height);
break;
case SWT.LEFT:
x = viewBounds.x - (viewBounds.width / 2);
y = viewBounds.y;
bounds = new Rectangle(x, y, viewBounds.width / 2, viewBounds.height);
break;
case SWT.TOP:
x = viewBounds.x;
y = viewBounds.y - (viewBounds.height / 2);
bounds = new Rectangle(x, y, viewBounds.width, viewBounds.height / 2);
break;
case SWT.BOTTOM:
x = viewBounds.x;
y = viewBounds.y + viewBounds.height;
bounds = new Rectangle(x, y, viewBounds.width, viewBounds.height / 2);
break;
default:
throw new IllegalArgumentException("position=" + position); //$NON-NLS-1$
}
shell = openShell(viewClazz, bounds);
if (shell != null) {
id2shell.put(id, shell);
}
} else {
showShell(shell);
}
}
/**
* Show the view / shell with the given id. If the given id matches no
* shell, then a new shell / view is created using the given viewClazz.
*
* @param id
* id of the view / shell to show; must be unique within this
* instance; not tied to the view-part id, since the view is
* created by reflection
* @param viewClazz
* the class of the view; must have parameterless constructor
* @param bounds
* the desired size and location for the shell. The {@code x} and
* {@code y} values set the upper left corner of the shell,
* relative to the display. Note that this is applied only if a
* new shell is created.
*/
public void showView(final String viewId, final Class<? extends ViewPart> viewClazz, final Rectangle bounds) {
Shell shell = id2shell.get(viewId);
if (shell == null) {
shell = openShell(viewClazz, bounds);
if (shell != null) {
id2shell.put(viewId, shell);
}
} else {
showShell(shell);
}
}
// protected methods
////////////////////
/**
* Determines the style bits for new shell instances created by this class.
* <p>
* Default value is: {@code SWT.NONE}
* <p>
* Implementors may override to use different style bits. Note that the
* close button will be disabled even if you use {@link SWT#CLOSE}.
*
* @see http://dev.eclipse.org/newslists/news.eclipse.tools/msg07666.html
*/
protected int getShellStyle() {
return SWT.NONE;
}
// helping methods
//////////////////
private void showShell(final Shell shell) {
shell.setVisible(true);
}
private Shell openShell(final Class<? extends ViewPart> viewClazz, final Rectangle bounds) {
Shell result = null;
result = new Shell(this.mainShell, getShellStyle());
result.addShellListener(new ShellAdapter() {
@Override
public void shellClosed(final ShellEvent e) {
e.doit = false; // prevent manual close just in case
}
});
try {
final IViewPart viewPart = ReflectionUtils.newInstance(viewClazz, (Object[]) null);
viewPart.createPartControl(result);
result.addDisposeListener(new DisposeListener() {
public void widgetDisposed(final DisposeEvent e) {
// dispose part when shell is disposed
viewPart.dispose();
}
});
if (!checkController(viewPart)) {
final String msg = String.format("%s does not override %s()", viewPart.getClass().getName(), //$NON-NLS-1$
METHOD_CREATE_CONTROLLER);
LOGGER.log(LogService.LOG_WARNING, msg);
}
} catch (final Exception exc) {
// calling unknown code - catch exception just in case
final String msg = "Exception while creating view: " + viewClazz.getName(); //$NON-NLS-1$
LOGGER.log(LogService.LOG_ERROR, msg, exc);
}
result.setBounds(bounds);
result.open();
return result;
}
/**
* Non-API. Package public for testing
*
* @return true if the check passed; false otherwise.
*/
@SuppressWarnings("rawtypes")
boolean checkController(final IViewPart viewPart) {
boolean result = true;
final Class clazz = viewPart.getClass();
final Method method = findMethod(clazz, METHOD_CREATE_CONTROLLER);
if (method != null) {
final String declClazz = method.getDeclaringClass().getName();
if (declClazz.equals("org.eclipse.riena.navigation.ui.swt.views.SubModuleView")) { //$NON-NLS-1$
result = false;
}
}
return result;
}
/**
* Return the first Method matching {@code methodName} or null. Will start
* at the most specific class and search upwards.
*/
private Method findMethod(final Class<?> viewClazz, final String methodName) {
Method result = null;
final List<Method> declaredMethods = new ArrayList<Method>();
Class<?> clazz = viewClazz;
while (clazz != null) {
declaredMethods.addAll(Arrays.asList(clazz.getDeclaredMethods()));
clazz = clazz.getSuperclass();
}
for (int i = 0; result == null && i < declaredMethods.size(); i++) {
final Method method = declaredMethods.get(i);
if (methodName.equals(method.getName())) {
result = method;
}
}
return result;
}
}