/******************************************************************************* * Copyright (c) 2006-2013 The RCP Company 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: * The RCP Company - initial API and implementation *******************************************************************************/ package com.rcpcompany.uibindings.internal.sourceProviders; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.eclipse.core.databinding.observable.ChangeEvent; import org.eclipse.core.databinding.observable.IChangeListener; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.expressions.IEvaluationContext; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.Widget; import org.eclipse.ui.AbstractSourceProvider; import org.eclipse.ui.ISources; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.contexts.IContextActivation; import org.eclipse.ui.contexts.IContextService; import com.rcpcompany.uibindings.Constants; import com.rcpcompany.uibindings.IBinding; import com.rcpcompany.uibindings.IBindingContext; import com.rcpcompany.uibindings.IManager; import com.rcpcompany.uibindings.ISourceProviderStateContext; import com.rcpcompany.uibindings.IUIBindingDecoratorExtender; import com.rcpcompany.uibindings.IUIBindingDecoratorExtenderDescriptor; import com.rcpcompany.uibindings.IValueBinding; import com.rcpcompany.uibindings.internal.Activator; import com.rcpcompany.uibindings.model.utils.BasicUtils; import com.rcpcompany.uibindings.utils.IManagerRunnable; import com.rcpcompany.utils.basic.ClassUtils; import com.rcpcompany.utils.basic.ui.TSSWTUtils; import com.rcpcompany.utils.extensionpoints.CEObjectHolder; import com.rcpcompany.utils.logging.LogUtils; /** * Source Provider for the UI Binding framework. * <p> * As a side effect this source provider also activates/deactivates UI contexts - see * {@link Constants#CONTAINER_CONTEXT_ID} and {@link Constants#WIDGET_CONTEXT_ID}. * <p> * This class has a number of interfaces: * <dl> * <dt>{@link AbstractSourceProvider}</dt> * <dd>Used by the expression framework.</dd> * <dt>{@link BindingSourceProvider#getCurrentState(Event)}</dt> * <dd>Calculates a new state based on the event, but it does <em>not</em> alter the global state.</dd> * <dt>{@link BindingSourceProvider#reportSourceChanges(Event)}</dt> * <dd>Calculates a new state based on the event, <em>and</em> alters the global state.</dd> * </dl> * The provider uses a number of SWT events and if the current focus control is a {@link Table} or * {@link Tree}, it also monitors the current selection. * * @author Tonny Madsen, The RCP Company */ public class BindingSourceProvider extends AbstractSourceProvider { /** * The names of the sources supported by this source provider. * <p> * <b>NOTE:</b> If you update this list, remember to update the services extension as well! This * is tested!!! */ public static final String[] PROVIDED_SOURCE_NAMES = new String[] { Constants.SOURCES_THE_MANAGER, Constants.SOURCES_ACTIVE_CONTEXT, Constants.SOURCES_ACTIVE_BINDING, Constants.SOURCES_ACTIVE_BINDING_TYPE, Constants.SOURCES_ACTIVE_BINDING_MODEL_OBJECT, Constants.SOURCES_ACTIVE_BINDING_FEATURE, Constants.SOURCES_ACTIVE_BINDING_RO, Constants.SOURCES_ACTIVE_BINDING_UNSETTABLE, Constants.SOURCES_ACTIVE_BINDING_OPEN_COMMAND, Constants.SOURCES_ACTIVE_BINDING_VALUE, Constants.SOURCES_ACTIVE_BINDING_VALUE_DISPLAY, Constants.SOURCES_ACTIVE_CONTAINER_BINDING, Constants.SOURCES_ACTIVE_CONTAINER_BINDING_NO_CAF, Constants.SOURCES_ACTIVE_CONTAINER_CELL_TYPE, Constants.SOURCES_ACTIVE_VIEWER_ELEMENT, Constants.SOURCES_ACTIVE_VIEWER_ELEMENT_TYPE, Constants.SOURCES_ACTIVE_VIEWER_ELEMENT_MOVE_UP, Constants.SOURCES_ACTIVE_VIEWER_ELEMENT_MOVE_DOWN, }; /** * Constructs and returns a new source provider. */ public BindingSourceProvider() { Display.getCurrent().addFilter(SWT.FocusIn, myDisplayFilter); Display.getCurrent().addFilter(SWT.MouseDown, myDisplayFilter); Display.getCurrent().addFilter(SWT.KeyUp, myDisplayFilter); Display.getCurrent().addFilter(SWT.MenuDetect, myDisplayFilter); resetMap(myCurrentState); } @Override public void dispose() { Display.getCurrent().removeFilter(SWT.FocusIn, myDisplayFilter); Display.getCurrent().removeFilter(SWT.MouseDown, myDisplayFilter); Display.getCurrent().removeFilter(SWT.KeyUp, myDisplayFilter); Display.getCurrent().removeFilter(SWT.MenuDetect, myDisplayFilter); } /** * The manager itself. */ private final IManager theManager = IManager.Factory.getManager(); /** * Internal filter for the display used to track the current binding information. * <p> * Please be extremely careful in this code to be fast and bail out as quickly as possible. */ private final Listener myDisplayFilter = new Listener() { @Override public void handleEvent(Event event) { reportSourceChanges(event); } }; /** * The previous state reported by the provider. */ private final Map<String, Object> myCurrentState = new HashMap<String, Object>(); /** * The last widget that was used for {@link #getCurrentState(Event)}. */ private Widget myLastWidget = null; /** * The current selection provider. * <p> * Changes in the selection prompts for a re-calculation of the sources. */ protected ISelectionProvider myCurrentSelectionProvider = null; /** * The listener used for {@link #myCurrentSelectionProvider}. */ protected ISelectionChangedListener myCurrentSelectionProviderListener = new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { if (Activator.getDefault().TRACE_SOURCE_PROVIDER_VERBOSE) { LogUtils.debug(this, "new selection: " + event.getSelection()); if (event.getSelection().isEmpty()) { LogUtils.debug(this, "empty"); } } reportSourceChanges(myPreviousValueEvent); } }; /** * The UI Context service. * * * TODO: the the context service via the current IBindingContext */ private final IContextService myContextService = (IContextService) PlatformUI.getWorkbench().getService( IContextService.class); /** * The last observable value that has been observed... * <p> * When/if the value changes, the state is recalculated... * * @see #observe(Event, List) */ private final List<IObservableValue> myPreviousValues = new ArrayList<IObservableValue>(); /** * The event associated with {@link #myPreviousValues}. */ private Event myPreviousValueEvent; /** * A listener that monitors changes in {@link #myPreviousValues}. */ private final IChangeListener myObservedChangeListener = new IChangeListener() { @Override public void handleChange(ChangeEvent event) { reportSourceChanges(myPreviousValueEvent); } }; private IContextActivation myBaseContextContextActivation; private IContextActivation myWidgetContextContextActivation; private IContextActivation myContainerContextContextActivation; private IBinding myLastBinding; /** * Observes changes in the specified valuse with the associated event. * * @param event the event * @param newValues the observable values */ protected void observe(Event event, List<IObservableValue> newValues) { myPreviousValueEvent = event; if (newValues.equals(myPreviousValues)) return; for (final IObservableValue v : myPreviousValues) { v.removeChangeListener(myObservedChangeListener); } myPreviousValues.clear(); for (final IObservableValue v : newValues) { v.addChangeListener(myObservedChangeListener); } myPreviousValues.addAll(newValues); } /** * Checks if the current state have changed, and reports these. * * @param event the current event * @return the resulting map */ public Map<String, Object> reportSourceChanges(Event event) { if (Activator.getDefault().TRACE_SOURCE_PROVIDER_VERBOSE && Activator.getDefault().TRACE_EVENTS_SWT) { LogUtils.debug(this, (event == myPreviousValueEvent ? "REPLAY " : "") + TSSWTUtils.toString(event)); } /* * If the event is FocusIn for the very same widget as last time, but with x,y=0,0, then... * ignore it... It is seen whenever the current shell is send to front. */ if (event.type == SWT.FocusIn && event.widget == myLastWidget && event.x == 0 && event.y == 0) return myCurrentState; /* * Ignore all key up events, except those that navigate... */ // if (event.type == SWT.KeyUp && (SWT.KEYCODE_BIT & event.keyCode) == 0) { // return myOldState; // } final Map<String, Object> newState = getCurrentState(event); myLastWidget = event.widget; /* * Update the current state with changes - keeping them in newState as well. */ for (final Iterator<Map.Entry<String, Object>> is = newState.entrySet().iterator(); is.hasNext();) { final Map.Entry<String, Object> i = is.next(); final String s = i.getKey(); final Object n = i.getValue(); final Object o = myCurrentState.get(s); if (BasicUtils.equals(n, o)) { is.remove(); } else { myCurrentState.put(s, n); } } if (!newState.isEmpty()) { /* * Reset the property testers as well, when any of the values changes */ newState.put(Constants.PREFIX + Constants.PROPERTY_CAN_DELETE, true); newState.put(Constants.PREFIX + Constants.PROPERTY_CAN_DELETE_SELECTED_OBJECTS, true); if (Activator.getDefault().TRACE_SOURCE_PROVIDER) { final StringBuilder sb = new StringBuilder("Binding sources change(" + myLastBinding + ")"); for (final Map.Entry<String, Object> i : newState.entrySet()) { final String s = i.getKey(); sb.append("\n ").append(s).append('='); final Object v = i.getValue(); if (v == null) { sb.append("<null>"); } else if (v == IEvaluationContext.UNDEFINED_VARIABLE) { sb.append("<undef>"); } else { sb.append('\'').append(v.toString()).append('\'').append(" [") .append(ClassUtils.getLastClassName(v)).append(']'); } } LogUtils.debug(this, sb.toString()); } fireSourceChanged(ISources.ACTIVE_CURRENT_SELECTION, newState); /* * TODO: describe why * * TODO: only when changing? */ final Object activeBindingObject = myCurrentState.get(Constants.SOURCES_ACTIVE_BINDING); if (activeBindingObject instanceof IValueBinding) { final IValueBinding vb = (IValueBinding) activeBindingObject; IManagerRunnable.Factory.asyncExec("update", vb, new Runnable() { @Override public void run() { if (vb.isDisposed()) return; vb.updateBinding(); } }); } /* * And lastly, update the active contexts */ handleContextChanges(myCurrentState); } return myCurrentState; } /** * @param newState */ private void handleContextChanges(final Map<String, Object> newState) { /* * If inside a container binding, then activate the proper context */ final boolean cb = newState.get(Constants.SOURCES_ACTIVE_CONTAINER_BINDING) != IEvaluationContext.UNDEFINED_VARIABLE; if (cb && myContainerContextContextActivation == null) { myContainerContextContextActivation = myContextService.activateContext(Constants.CONTAINER_CONTEXT_ID); if (Activator.getDefault().TRACE_CONTEXTS) { LogUtils.debug(this, "activated " + Constants.CONTAINER_CONTEXT_ID); } } if (!cb && myContainerContextContextActivation != null) { myContextService.deactivateContext(myContainerContextContextActivation); myContainerContextContextActivation = null; if (Activator.getDefault().TRACE_CONTEXTS) { LogUtils.debug(this, "deactivated " + Constants.CONTAINER_CONTEXT_ID); } } /* * If inside an value binding, then activate the proper context */ final boolean vb = !cb && newState.get(Constants.SOURCES_ACTIVE_BINDING) != IEvaluationContext.UNDEFINED_VARIABLE; if (vb && myWidgetContextContextActivation == null) { myWidgetContextContextActivation = myContextService.activateContext(Constants.WIDGET_CONTEXT_ID); if (Activator.getDefault().TRACE_CONTEXTS) { LogUtils.debug(this, "activated " + Constants.WIDGET_CONTEXT_ID); } } if (!vb && myWidgetContextContextActivation != null) { myContextService.deactivateContext(myWidgetContextContextActivation); myWidgetContextContextActivation = null; if (Activator.getDefault().TRACE_CONTEXTS) { LogUtils.debug(this, "deactivated " + Constants.WIDGET_CONTEXT_ID); } } /* * Also activate the base context - this does not seem to happen automatically ??? */ final boolean bc = myContainerContextContextActivation != null || myWidgetContextContextActivation != null; if (bc && myBaseContextContextActivation == null) { myBaseContextContextActivation = myContextService.activateContext(Constants.COMMON_CONTEXT_ID); if (Activator.getDefault().TRACE_CONTEXTS) { LogUtils.debug(this, "activated " + Constants.COMMON_CONTEXT_ID); } } if (!bc && myBaseContextContextActivation != null) { myContextService.deactivateContext(myBaseContextContextActivation); myBaseContextContextActivation = null; if (Activator.getDefault().TRACE_CONTEXTS) { LogUtils.debug(this, "deactivated " + Constants.COMMON_CONTEXT_ID); } } } @Override public Map<String, Object> getCurrentState() { return myCurrentState; } /** * Returns a Map with the current state for the specific event if specified. * * @param event the current event - possibly <code>null</code> * @return a Map with the current state */ public Map<String, Object> getCurrentState(final Event event) { final Map<String, Object> map = new HashMap<String, Object>(); final List<IObservableValue> values = new ArrayList<IObservableValue>(); resetMap(map); if (event.type == SWT.MenuDetect) { // LogUtils.debug(event, "MenuDetect"); } try { myLastBinding = IBindingContext.Factory.getBindingForWidget(event.widget); if (myLastBinding == null) return map; map.put(Constants.SOURCES_ACTIVE_CONTEXT, myLastBinding.getContext()); final ISourceProviderStateContext context = new ISourceProviderStateContext() { @Override public Event getEvent() { return event; } private Point myLocation = null; @Override public Point getLocation() { if (myLocation == null) { myLocation = new Point(event.x, event.y); switch (event.type) { case SWT.MenuDetect: /* * The location is relative to the display */ myLocation = event.widget.getDisplay().map(null, (Control) event.widget, myLocation); break; default: break; } } return myLocation; } @Override public Map<String, Object> getState() { return map; } @Override public void putSourceValue(String name, Object value) { map.put(name, value); } @Override public void addObservedValue(IObservableValue value) { values.add(value); } @Override public void setSelectionProvider(ISelectionProvider provider) { if (provider == myCurrentSelectionProvider) return; if (myCurrentSelectionProvider != null) { myCurrentSelectionProvider.removeSelectionChangedListener(myCurrentSelectionProviderListener); } myCurrentSelectionProvider = provider; if (myCurrentSelectionProvider != null) { myCurrentSelectionProvider.addSelectionChangedListener(myCurrentSelectionProviderListener); } } }; try { myLastBinding.updateSourceProviderState(context); } catch (final Exception ex) { LogUtils.error(myLastBinding, ex); } /* * If the active binding is a value binding, run all extenders as well... */ final Object activeBinding = map.get(Constants.SOURCES_ACTIVE_BINDING); if (activeBinding instanceof IValueBinding) { final IValueBinding vb = (IValueBinding) activeBinding; for (final IUIBindingDecoratorExtenderDescriptor d : IManager.Factory.getManager() .getDecoratorExtenders()) { final CEObjectHolder<IUIBindingDecoratorExtender> factory = d.getFactory(); final IUIBindingDecoratorExtender extender = factory.getObject(); if (extender == null) { LogUtils.error(factory.getConfigurationElement(), "Cannot create extender"); continue; } try { if (!extender.isEnabled(vb)) { continue; } extender.updateSourceProviderState(vb, context); } catch (final Exception ex) { LogUtils.error(factory.getConfigurationElement(), ex); } } } observe(event, values); } catch (final Exception ex) { LogUtils.error(this, ex); } return map; } private void resetMap(final Map<String, Object> map) { map.put(Constants.SOURCES_THE_MANAGER, theManager); map.put(Constants.SOURCES_ACTIVE_CONTEXT, IEvaluationContext.UNDEFINED_VARIABLE); map.put(Constants.SOURCES_ACTIVE_CONTAINER_BINDING, IEvaluationContext.UNDEFINED_VARIABLE); map.put(Constants.SOURCES_ACTIVE_CONTAINER_CELL_TYPE, IEvaluationContext.UNDEFINED_VARIABLE); map.put(Constants.SOURCES_ACTIVE_CONTAINER_BINDING_NO_CAF, false); map.put(Constants.SOURCES_ACTIVE_VIEWER_ELEMENT, IEvaluationContext.UNDEFINED_VARIABLE); map.put(Constants.SOURCES_ACTIVE_VIEWER_ELEMENT_TYPE, IEvaluationContext.UNDEFINED_VARIABLE); map.put(Constants.SOURCES_ACTIVE_VIEWER_ELEMENT_MOVE_UP, false); map.put(Constants.SOURCES_ACTIVE_VIEWER_ELEMENT_MOVE_DOWN, false); map.put(Constants.SOURCES_ACTIVE_BINDING, IEvaluationContext.UNDEFINED_VARIABLE); map.put(Constants.SOURCES_ACTIVE_BINDING_TYPE, IEvaluationContext.UNDEFINED_VARIABLE); map.put(Constants.SOURCES_ACTIVE_BINDING_MODEL_OBJECT, IEvaluationContext.UNDEFINED_VARIABLE); map.put(Constants.SOURCES_ACTIVE_BINDING_FEATURE, IEvaluationContext.UNDEFINED_VARIABLE); map.put(Constants.SOURCES_ACTIVE_BINDING_RO, true); map.put(Constants.SOURCES_ACTIVE_BINDING_UNSETTABLE, false); map.put(Constants.SOURCES_ACTIVE_BINDING_OPEN_COMMAND, IEvaluationContext.UNDEFINED_VARIABLE); map.put(Constants.SOURCES_ACTIVE_BINDING_VALUE, IEvaluationContext.UNDEFINED_VARIABLE); map.put(Constants.SOURCES_ACTIVE_BINDING_VALUE_DISPLAY, IEvaluationContext.UNDEFINED_VARIABLE); } @Override public String[] getProvidedSourceNames() { return PROVIDED_SOURCE_NAMES; } }