/*******************************************************************************
* 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.navigation.ui.swt.views;
import java.beans.Beans;
import java.util.HashMap;
import java.util.Map;
import org.osgi.service.log.LogService;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.Platform;
import org.eclipse.equinox.log.Logger;
import org.eclipse.jface.window.IShellProvider;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.ISourceProvider;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.services.ISourceProviderService;
import org.eclipse.riena.core.Log4r;
import org.eclipse.riena.core.util.InvocationTargetFailure;
import org.eclipse.riena.core.util.StringUtils;
import org.eclipse.riena.internal.navigation.ui.swt.Activator;
import org.eclipse.riena.internal.navigation.ui.swt.handlers.NavigationSourceProvider;
import org.eclipse.riena.navigation.ApplicationNodeManager;
import org.eclipse.riena.navigation.IApplicationNode;
import org.eclipse.riena.navigation.INavigationNode;
import org.eclipse.riena.navigation.ISubModuleNode;
import org.eclipse.riena.navigation.NavigationNodeId;
import org.eclipse.riena.navigation.listener.NavigationTreeObserver;
import org.eclipse.riena.navigation.listener.SubModuleNodeListener;
import org.eclipse.riena.navigation.model.SubModuleNode;
import org.eclipse.riena.navigation.ui.controllers.SubModuleController;
import org.eclipse.riena.navigation.ui.swt.presentation.SwtViewProvider;
import org.eclipse.riena.navigation.ui.swt.presentation.stack.TitlelessStackPresentation;
import org.eclipse.riena.ui.ridgets.IRidget;
import org.eclipse.riena.ui.ridgets.IRidgetContainer;
import org.eclipse.riena.ui.ridgets.SubModuleUtils;
import org.eclipse.riena.ui.ridgets.controller.IController;
import org.eclipse.riena.ui.ridgets.swt.uibinding.AbstractViewBindingDelegate;
import org.eclipse.riena.ui.ridgets.swt.uibinding.DefaultSwtBindingDelegate;
import org.eclipse.riena.ui.ridgets.swt.views.BlockHelper;
import org.eclipse.riena.ui.swt.EmbeddedTitleBar;
import org.eclipse.riena.ui.swt.facades.SWTFacade;
import org.eclipse.riena.ui.swt.lnf.LnFUpdater;
import org.eclipse.riena.ui.swt.lnf.LnfKeyConstants;
import org.eclipse.riena.ui.swt.lnf.LnfManager;
import org.eclipse.riena.ui.swt.utils.SWTBindingPropertyLocator;
import org.eclipse.riena.ui.swt.utils.SWTControlFinder;
import org.eclipse.riena.ui.swt.utils.SwtUtilities;
import org.eclipse.riena.ui.swt.utils.WidgetIdentificationSupport;
import org.eclipse.riena.ui.workarea.IWorkareaDefinition;
import org.eclipse.riena.ui.workarea.WorkareaManager;
/**
* Abstract implementation for a sub module view
*/
public abstract class SubModuleView extends ViewPart implements INavigationNodeView<ISubModuleNode> {
/**
* @since 3.0
*/
public static final String SHARED_ID = "shared"; //$NON-NLS-1$
private final static Logger LOGGER = Log4r.getLogger(Activator.getDefault(), SubModuleView.class);
private final LnFUpdater lnfUpdater = LnFUpdater.getInstance();
private final static Map<SubModuleView, SubModuleNode> FALLBACK_NODES = new HashMap<SubModuleView, SubModuleNode>();
/**
* The key of the SWT data property that identifies the (top) composite of a sub-module view.
*/
private static final String IS_SUB_MODULE_VIEW_COMPOSITE = "isSubModuleViewComposite"; //$NON-NLS-1$
private final AbstractViewBindingDelegate binding;
private SubModuleController currentController;
/**
* This node is used when creating this ViewPart inside an RCP application. It is created with information from the extension registry, instead being
* obtained from the navigation tree.
*
* @see #getRCPSubModuleNode()
*/
private SubModuleNode rcpSubModuleNode;
/** The title bar at the top of the view. May be null if running in RCP */
private EmbeddedTitleBar title;
private Composite parentComposite;
private Composite contentComposite;
private NavigationTreeObserver navigationTreeObserver;
private NavigationSourceProvider navigationSourceProvider;
private SubModuleNodesListener subModuleNodeListener;
/**
* used for the e4 migration
* <p>
* Setting this field to something not <code>null</code> ensures that the getNavigationNode() method returns this value. If this value is <code>null</code>
* the method behaves like implemented in Riena for Eclipse 3.x
*/
private ISubModuleNode navigationNode;
/**
* used for the e4 migration
*/
private IShellProvider shellProvider;
private boolean e4Runtime = false;
private final BlockHelper blockHelper;
/**
* Creates a new instance of {@code SubModuleView}.
*/
public SubModuleView() {
blockHelper = new BlockHelper() {
@Override
protected IRidgetContainer getController() {
return SubModuleView.this.getController();
}
@Override
protected Control getParentComposite() {
return SubModuleView.this.getParentComposite();
}
@Override
protected boolean shouldRestoreFocus() {
return SubModuleView.this.shouldRestoreFocus();
}
@Override
protected IRidget getFocusRidget() {
return SubModuleView.this.getFocusRidget();
}
};
binding = createBinding();
setShellProvider(new IShellProvider() {
public Shell getShell() {
return getSite().getShell();
}
});
}
/**
* @since 5.0
*/
public void setE4Runtime(final boolean e4Runtime) {
this.e4Runtime = e4Runtime;
}
public void addUpdateListener(final IComponentUpdateListener listener) {
throw new UnsupportedOperationException();
}
/**
* @since 5.0
*/
public void bind(final ISubModuleNode node) {
// create new controller if not existent for new node
if ((getNavigationNode() != null) && (getController() == null)) {
createViewFacade();
}
// Different node?
if (currentController != getController()) {
//"old" node bound?
if (currentController != null) {
// old node disposed?
if (currentController.getNavigationNode().isDisposed() && !e4Runtime) {
return;
}
// unbind "old" node
binding.unbind(currentController);
}
if (getController() != null) {
currentController = getController();
}
lnfUpdater.updateUIControlColors(getContentComposite());
//bind the new controller
binding.bind(currentController);
//callback
currentController.afterBind();
}
lnfUpdater.updateUIControlsAfterBind(getContentComposite());
//TODO is this really part of the the binding of the SubModuleView? NavigationSourceProvider is Menu-specific and should be handled in a context of Menus
activeNodeChanged(getNavigationNode());
//set block state on bind
blockView(node.isBlocked());
}
private void activeNodeChanged(final INavigationNode<?> node) {
//TODO move logic to Menu
if (navigationSourceProvider == null && getSite() != null) {
navigationSourceProvider = getNavigationSourceProvider();
}
if (navigationSourceProvider != null) {
if (!navigationSourceProvider.isDisposed()) {
navigationSourceProvider.activeNodeChanged(node);
} else {
navigationSourceProvider = null;
}
}
}
private NavigationSourceProvider getNavigationSourceProvider() {
final ISourceProviderService sourceProviderService = (ISourceProviderService) getSite().getService(ISourceProviderService.class);
if (sourceProviderService == null) {
return null;
}
final ISourceProvider[] sourceProviders = sourceProviderService.getSourceProviders();
for (final ISourceProvider sourceProvider : sourceProviders) {
if (sourceProvider instanceof NavigationSourceProvider) {
return (NavigationSourceProvider) sourceProvider;
}
}
return null;
}
@Override
public void createPartControl(final Composite parent) {
this.parentComposite = parent;
//markup for SubModuleViews. The StackPresentation uses this information for layouting
parent.setData(IS_SUB_MODULE_VIEW_COMPOSITE, Boolean.TRUE);
if (!Beans.isDesignTime()) {
//observe the navigation model to get activity change events
observeRoot();
final SubModuleController controller = createController(getNavigationNode());
if (controller != null) {
setPartName(controller.getNavigationNode().getLabel());
}
// the content composite is where all controls of the view are placed
contentComposite = createContentComposite(parent);
} else {
contentComposite = parent;
}
blockHelper.registerOnContentComposite(contentComposite);
contentComposite.setData(TitlelessStackPresentation.DATA_KEY_CONTENT_COMPOSITE, true);
createWorkarea(contentComposite);
if (Beans.isDesignTime()) {
lnfUpdater.updateUIControls(getParentComposite(), true);
} else {
registerView();
createViewFacade();
doBinding();
}
if (getViewSite() != null) {
if (getViewSite().getSecondaryId() != null) {
WidgetIdentificationSupport.setIdentification(contentComposite, "subModuleView", getViewSite().getId(), getViewSite().getSecondaryId()); //$NON-NLS-1$
}
}
}
/**
* @since 3.0
*/
protected void registerView() {
if (getViewSite() != null) {
final String id = getViewSite().getId();
final String secondaryId = getViewSite().getSecondaryId();
SwtViewProvider.getInstance().registerView(id, secondaryId, this);
}
}
@Override
public void dispose() {
final IApplicationNode appNode = getAppNode();
if (navigationTreeObserver != null && appNode != null) {
navigationTreeObserver.removeListenerFrom(appNode);
}
FALLBACK_NODES.remove(this);
destroyView();
}
/**
* @since 3.0
*/
protected void destroyView() {
super.dispose();
if (getViewSite() != null) {
final String id = getViewSite().getId();
final String secondaryId = getViewSite().getSecondaryId();
SwtViewProvider.getInstance().unregisterView(id, secondaryId);
}
}
/**
* @return the controller
*/
public SubModuleController getController() {
if (getNavigationNode() != null && getNavigationNode().getNavigationNodeController() instanceof SubModuleController) {
return (SubModuleController) getNavigationNode().getNavigationNodeController();
}
return null;
}
/**
* @since 5.0
*/
public ISubModuleNode getNavigationNode() {
if (navigationNode != null) {
return navigationNode;
}
if (getViewSite() == null) {
return getFallbackNavigationNode();
}
final String viewId = this.getViewSite().getId();
final String secondaryId = this.getViewSite().getSecondaryId();
SubModuleNode result = (SubModuleNode) getSubModuleNode(viewId, secondaryId);
if (result == null) {
result = getRCPSubModuleNode();
}
if (result == null) {
result = getFallbackNavigationNode();
}
return result;
}
/**
* Important: This method is NOT API. It is used for the e4 migration only.
*
* @param node
* @since 5.0
* @see SubModuleView#navigationNode
*/
public void setNavigationNode(final ISubModuleNode navigationNode) {
this.navigationNode = navigationNode;
}
/**
* This implementation will automatically focus on the control that had previously the focus, or, the first focusable control.
* <p>
* You may overwrite it, but it typically is not necessary to do so. If you still want to use the 'restore focus to last control' functionality, check
* {@link #canRestoreFocus()} and the invoke this method.
*/
@Override
public void setFocus() {
blockHelper.setFocus();
}
private IRidget getFocusRidget() {
return currentController != null ? currentController.getFocusableRidget() : null;
}
public void unbind() {
final SubModuleController controller = getController();
if (controller != null) {
binding.unbind(controller);
}
}
/**
* Adds the given control to the list of the controls that will be binded.
*
* @param uiControl
* control to bind
*/
protected void addUIControl(final Object uiControl) {
binding.addUIControl(uiControl);
}
/**
* Adds the given control to the list of the controls that will be binded.
*
* @param uiControl
* control to bind
* @param bindingId
* ID for binding
*/
protected void addUIControl(final Object uiControl, final String bindingId) {
binding.addUIControl(uiControl, bindingId);
}
/**
* Is called by the SubModuleView after {@link #basicCreatePartControl(Composite)}
*
* @param parent
* @since 1.2
*/
protected void afterBasicCreatePartControl(final Composite parent) {
}
/**
* Creates the content of the sub module view.
*
* @param parent
* composite for the content of the sub module view
*/
protected abstract void basicCreatePartControl(Composite parent);
protected void blockView(final boolean block) {
blockHelper.setBlocked(block);
}
/**
* @since 5.0
*/
protected IShellProvider getShellProvider() {
return shellProvider;
}
private void setShellProvider(final IShellProvider shellProvider) {
this.shellProvider = shellProvider;
}
protected final boolean canRestoreFocus() {
return blockHelper.canRestoreFocus();
}
/**
* @since 3.0
*/
protected boolean shouldRestoreFocus() {
if (getController() != null && getController().getModuleController() instanceof SWTModuleController) {
return !((SWTModuleController) getController().getModuleController()).getTree().hasFocus() && getController().isActivated();
} else {
return true;
}
}
/**
* Creates a delegate for the binding of view and controller.
*
* @return delegate for binding
*/
protected AbstractViewBindingDelegate createBinding() {
return new DefaultSwtBindingDelegate();
}
protected SubModuleController createController(final ISubModuleNode node) {
// check node itself for controller definition first
Assert.isNotNull(node, "navigation node must not be null"); //$NON-NLS-1$
Assert.isNotNull(node.getNodeId(), "navigation node id must not be null"); //$NON-NLS-1$
Assert.isNotNull(node.getNodeId().getTypeId(), "navigation node type id must not be null"); //$NON-NLS-1$
SubModuleController controller = null;
if (!SubModuleUtils.isPrepareView()) {
controller = getController();
}
if (controller == null) {
// consult workarea manager
final IWorkareaDefinition def = WorkareaManager.getInstance().getDefinition(node);
if (def != null) {
try {
controller = (SubModuleController) def.createController();
} catch (final Exception ex) {
final String message = String.format("cannnot create controller for class %s", def.getControllerClass()); //$NON-NLS-1$
LOGGER.log(LogService.LOG_ERROR, message, ex);
throw new InvocationTargetFailure(message, ex);
}
}
}
if (controller != null) {
controller.setNavigationNode(node);
}
return controller;
}
protected void createViewFacade() {
// add controls of parent
addUIControls(getParentComposite());
if (getController() == null) {
createController(getNavigationNode());
}
//only bind if controller is available
if (getController() != null) {
binding.injectRidgets(getController());
}
}
/**
* Creates the workarea. Subclasses can override this method to get full control over the workarea layout.
*
* @param parent
* @since 1.2
*/
protected void createWorkarea(final Composite parent) {
basicCreatePartControl(parent);
afterBasicCreatePartControl(parent);
}
protected Composite getContentComposite() {
return contentComposite;
}
protected Composite getParentComposite() {
// the composite created by the Workbench given to the ViewPart
return parentComposite;
}
/**
* Find the navigation node corresponding to the passed ids. If the view is shared the {@link INavigationNode#isActivated()} state has to be considered
* because there can be multiple nodes matching the nodeId and secondaryId. Only the active node counts!
*
* @param nodeId
* the id of the node
* @param secondaryId
* the secondary id
* @return the subModule node if found
*/
protected ISubModuleNode getSubModuleNode(final String nodeId, final String secondaryId) {
final boolean ignoreSharedState = secondaryId == null || !secondaryId.startsWith(SubModuleView.SHARED_ID);
return SwtViewProvider.getInstance().getNavigationNode(nodeId, secondaryId, ISubModuleNode.class, ignoreSharedState);
}
// helping methods
//////////////////
private void addMenuControl(final Menu menu) {
final SWTBindingPropertyLocator locator = SWTBindingPropertyLocator.getInstance();
for (int i = 0; i < menu.getItemCount(); i++) {
final MenuItem item = menu.getItem(i);
final String bindingId = locator.locateBindingProperty(item);
if (StringUtils.isGiven(bindingId)) {
addUIControl(item, bindingId);
}
if (item.getMenu() != null) {
addMenuControl(item.getMenu());
}
}
}
/**
* @since 3.0
*/
protected void addUIControls(final Composite composite) {
final SWTControlFinder finder = new SWTControlFinder(composite) {
@Override
public void handleBoundControl(final Control control, final String bindingProperty) {
addUIControl(control);
}
@Override
public void handleControl(final Control control) {
if (control.getMenu() != null) {
addMenuControl(control.getMenu());
}
super.handleControl(control);
}
};
finder.run();
}
/**
* Creates the composite for the content of the view. Its a container that holds the UI controls of the view.<br>
* Above this container the title bar of the view is located.
*
* @param parent
* @return
*/
private Composite createContentComposite(final Composite parent) {
final Color bgColor = LnfManager.getLnf().getColor(LnfKeyConstants.SUB_MODULE_BACKGROUND);
parent.setBackground(bgColor);
parent.setLayout(new FormLayout());
if (!isRCP()) {
title = new EmbeddedTitleBar(parent, SWT.NONE);
addUIControl(title, SubModuleController.WINDOW_RIDGET);
// as the view is only shown if the node is active we should never have to change the "window active" state of the titlebar
title.setWindowActive(true);
final FormData formData = new FormData();
// don't show the top border of the title => -1
formData.top = new FormAttachment(0, -1);
// don't show the left border of the title => -1
formData.left = new FormAttachment(0, -1);
// don't show the top border of the title, but show the bottom
// border => -1
formData.bottom = new FormAttachment(0, title.getSize().y - 1);
// don't show the right border of the title => 1
formData.right = new FormAttachment(100, 1);
title.setLayoutData(formData);
SWTFacade.getDefault().createEmbeddedTitleBarToolTip(title);
}
final Composite composite = new Composite(parent, SWT.DOUBLE_BUFFERED);
composite.setBackground(bgColor);
final FormData formData = new FormData();
if (title != null) {
formData.top = new FormAttachment(title, 0, 0);
} else {
formData.top = new FormAttachment(0, -1);
}
formData.left = new FormAttachment(0, 0);
formData.bottom = new FormAttachment(100);
formData.right = new FormAttachment(100);
composite.setLayoutData(formData);
return composite;
}
private void doBinding() {
bind(getNavigationNode());
}
/**
* @since 3.0
*/
protected IApplicationNode getAppNode() {
//use the ApplicationNodeManager API
return ApplicationNodeManager.getApplicationNode();
}
/**
* @return a fallback navigation node for views that are not associated with a node in the navigation tree.
*/
private SubModuleNode getFallbackNavigationNode() {
SubModuleNode fallbackNode = FALLBACK_NODES.get(this);
if (fallbackNode == null) {
fallbackNode = new SubModuleNode(new NavigationNodeId(getClass().getName() + FALLBACK_NODES.size()));
FALLBACK_NODES.put(this, fallbackNode);
}
return fallbackNode;
}
private SubModuleNode getRCPSubModuleNode() {
final IExtensionRegistry registry = Platform.getExtensionRegistry();
final IConfigurationElement[] elements = registry.getConfigurationElementsFor("org.eclipse.riena.navigation.assemblies"); //$NON-NLS-1$
final String viewId = getViewSite().getId();
return getRCPSubModuleNode(viewId, elements);
}
private SubModuleNode getRCPSubModuleNode(final String viewId, final IConfigurationElement[] elements) {
for (int i = 0; rcpSubModuleNode == null && i < elements.length; i++) {
final IConfigurationElement element = elements[i];
if ("submodule".equals(element.getName())) { //$NON-NLS-1$
final String view = element.getAttribute("view"); //$NON-NLS-1$
if (viewId.equals(view)) {
final String typeId = element.getAttribute("typeId"); //$NON-NLS-1$
if (typeId != null) {
rcpSubModuleNode = new SubModuleNode(new NavigationNodeId(typeId), getPartName());
}
}
} else if (element.getChildren().length > 0) {
rcpSubModuleNode = getRCPSubModuleNode(viewId, element.getChildren());
}
}
return rcpSubModuleNode;
}
/**
* Returns true if we are running without the navigation tree
*/
private boolean isRCP() {
//TODO: refactor testing for RCP
getNavigationNode();
return rcpSubModuleNode != null;
}
private void observeRoot() {
final IApplicationNode appNode = getAppNode();
if (appNode != null) {
Assert.isLegal(navigationTreeObserver == null);
navigationTreeObserver = new NavigationTreeObserver();
subModuleNodeListener = new SubModuleNodesListener();
navigationTreeObserver.addListener(subModuleNodeListener);
navigationTreeObserver.addListenerTo(appNode);
}
}
// helping classes
//////////////////
/**
* @since 3.0
*/
protected String getSecondaryId() {
return getViewSite().getSecondaryId();
}
/**
* @since 3.0
*/
protected void unbindActiveController() {
//unbind
binding.unbind(currentController);
//reset controller
currentController = null;
}
/**
* A listener for all submodules in the navigation tree! Needed i.e. to support shared views. When adding a method be sure to check the node.
*/
private final class SubModuleNodesListener extends SubModuleNodeListener {
@Override
public void activated(final ISubModuleNode source) {
if (source.equals(getNavigationNode())) {
if (SwtUtilities.isDisposed(parentComposite)) {
/*
* Do not bind disposed views. TODO For disposed views this listener should be unregistered.
*/
return;
}
doBinding();
}
}
@Override
public void beforeDisposed(final ISubModuleNode source) {
/*
* If source is the current bound node then unbind the controller. If the node is not bound (not the current) we do not have to unbind anything. In
* the case of detached views there�s no viewSite available!
*/
if (getViewSite() != null && disposingBoundNode(source)) {
unbindActiveController();
}
}
protected boolean disposingBoundNode(final ISubModuleNode source) {
/*
* First check if typeId fits. Then check if source is the current node.
*/
return getSecondaryId().startsWith(SHARED_ID) && currentController != null && source.equals(currentController.getNavigationNode());
}
@Override
public void block(final ISubModuleNode source, final boolean block) {
if (source.equals(getNavigationNode())) {
blockView(block);
}
}
@Override
public void nodeIdChange(final ISubModuleNode source, final NavigationNodeId oldId, final NavigationNodeId newId) {
if (source.equals(getNavigationNode())) {
//TODO: is this the right place for changing nodeId? There is no view-specific logic
SwtViewProvider.getInstance().replaceNavigationNodeId(source, oldId, newId);
}
}
}
/**
* Triggered by "prepareNode" for nodes to be prepared which already have an instantiated view. In those cases createPartControl is not called.
*
* @since 5.0
*/
public void prepareNode(final ISubModuleNode node) {
final IController controller = createController(node);
if (controller != null) {
binding.injectRidgets(controller);
}
}
}