/*******************************************************************************
* 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.bindingMessages;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.databinding.Binding;
import org.eclipse.core.databinding.observable.ChangeEvent;
import org.eclipse.core.databinding.observable.IChangeListener;
import org.eclipse.core.databinding.observable.IObservable;
import org.eclipse.core.databinding.observable.list.IObservableList;
import org.eclipse.core.databinding.observable.list.WritableList;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.runtime.Assert;
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.fieldassist.FieldDecoration;
import org.eclipse.jface.fieldassist.FieldDecorationRegistry;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import com.rcpcompany.uibindings.BindingMessageSeverity;
import com.rcpcompany.uibindings.BindingState;
import com.rcpcompany.uibindings.Constants;
import com.rcpcompany.uibindings.IBindingMessage;
import com.rcpcompany.uibindings.IBindingMessage.FeatureMatchingAlgorithm;
import com.rcpcompany.uibindings.IDisposable;
import com.rcpcompany.uibindings.IManager;
import com.rcpcompany.uibindings.IQuickfixProposal;
import com.rcpcompany.uibindings.IUIBindingDecoratorExtender;
import com.rcpcompany.uibindings.IUIBindingDecoratorExtenderContext;
import com.rcpcompany.uibindings.IUIBindingsPackage;
import com.rcpcompany.uibindings.IValueBinding;
import com.rcpcompany.uibindings.contextAdapters.ContextMessageDecorator;
import com.rcpcompany.uibindings.decorators.extenders.AbstractUIBindingDecoratorExtender;
import com.rcpcompany.uibindings.internal.Activator;
import com.rcpcompany.uibindings.internal.Messages;
import com.rcpcompany.uibindings.internal.bindingMessages.messageAdapters.DataBindingDecoratorMessageObservableValue;
import com.rcpcompany.uibindings.internal.observables.DelayedChangeEvent;
import com.rcpcompany.uibindings.internal.observables.IDelayedChangeListener;
import com.rcpcompany.uibindings.internal.observables.IDelayedChangeObservable;
import com.rcpcompany.uibindings.internal.observables.TextObservableValue;
import com.rcpcompany.uibindings.model.utils.BasicUtils;
import com.rcpcompany.uibindings.observables.IKeyedObservable;
import com.rcpcompany.uibindings.utils.IManagerRunnable;
import com.rcpcompany.uibindings.validators.IValidatorAdapterManager;
import com.rcpcompany.uibindings.validators.IValidatorAdapterMessageDecorator;
import com.rcpcompany.utils.logging.LogUtils;
/**
* Message decorator for {@link IValueBinding} objects.
* <p>
* One of these exists for each {@link IValueBinding} object so some care must be taken to limit the
* amount of data in the object.
*
* @author Tonny Madsen, The RCP Company
*/
public class ValueBindingMessageImageDecorator extends AdapterImpl implements IDisposable, IContextMessageProvider,
IValidatorAdapterMessageDecorator, Adapter, IChangeListener, IDelayedChangeListener, Listener {
/**
* Extender for {@link ValueBindingMessageImageDecorator}.
*/
public static class Extender extends AbstractUIBindingDecoratorExtender implements IUIBindingDecoratorExtender {
@Override
public void extend(IUIBindingDecoratorExtenderContext context) {
final IValueBinding binding = context.getBinding();
ValueBindingMessageImageDecorator data = binding.getService(ValueBindingMessageImageDecorator.class);
if (data == null) {
data = new ValueBindingMessageImageDecorator(binding);
}
data.extend(context);
}
}
/**
* The {@link IValueBinding value binding} of this decorator.
*/
private final IValueBinding myBinding;
/**
* Constructs and returns a new decorator.
*
* @param binding the value binding
*/
public ValueBindingMessageImageDecorator(IValueBinding binding) {
myBinding = binding;
myObservedObject = getBinding().getModelObject();
init();
}
/**
* Completes the initialization of this message decorator. Only invoked when the state of the
* binding is OK.
*/
protected void init() {
if (getBinding().getState() != BindingState.OK) {
/*
* If not in state OK, then wait until we get there...
*/
getBinding().eAdapters().add(this);
return;
}
Assert.isTrue(getBinding().getState() == BindingState.OK);
if (Activator.getDefault().TRACE_LIFECYCLE_VALUE_BINDING_MESSAGE_DECORATOR) {
LogUtils.debug(this, "init " + hashCode() + ": " + getBinding()); //$NON-NLS-1$ //$NON-NLS-2$
}
getBinding().registerService(this);
IObservable observable = getBinding().getUIObservable();
observable.addChangeListener(this);
if (observable instanceof IDelayedChangeObservable) {
((IDelayedChangeObservable) observable).addDelayedChangeListener(this);
}
observable = getBinding().getModelObservable();
observable.addChangeListener(this);
final IObservableList decoratorMessages = getBinding().getDecorator().getMessages();
if (decoratorMessages != null) {
decoratorMessages.addChangeListener(this);
}
/*
* The list change listener will ensure that the change listener is added to all the message
* providers
*/
final List<Binding> bindings = getBinding().getMonitoredDBBindings();
myMessageProviders = new IObservableValue[bindings.size()];
for (int i = 0; i < bindings.size(); i++) {
// TODO TMTM reduce the extra observable!
myMessageProviders[i] = new DataBindingDecoratorMessageObservableValue(getBinding(), bindings.get(i));
myMessageProviders[i].addChangeListener(this);
}
/*
* Register this decorator with the ValidatorAdapterManager.
*/
VALIDATION_MANAGER.addDecorator(this);
/*
* Add ourself as a message decorator to the context if present
*/
final ContextMessageDecorator contextMessageDecorator = myBinding.getContext().getService(
ContextMessageDecorator.class);
if (contextMessageDecorator != null) {
contextMessageDecorator.addMessageProvider(this);
}
/*
* For bindings with controls, register a focus listener, so we can change the decoration at
* focus in/out.
*/
final Control control = getBinding().getControl();
if (control != null) {
control.addListener(SWT.FocusIn, this);
control.addListener(SWT.FocusOut, this);
}
/*
* Register for configuration changes
*/
IManager.Factory.getManager().eAdapters().add(this);
}
/**
* Disposes of this binding message decoration.
*/
@Override
public void dispose() {
IManagerRunnable.Factory.cancelAsyncExec("updateDecorations", this);
VALIDATION_MANAGER.removeDecorator(this);
IObservable observable = getBinding().getUIObservable();
observable.removeChangeListener(this);
if (observable instanceof IDelayedChangeObservable) {
((IDelayedChangeObservable) observable).removeDelayedChangeListener(this);
}
observable = getBinding().getModelObservable();
observable.removeChangeListener(this);
getBinding().unregisterService(this);
final Control control = getBinding().getControl();
if (control != null) {
control.removeListener(SWT.FocusIn, this);
control.removeListener(SWT.FocusOut, this);
}
/*
* Clear all messages and update the context decorator...
*/
myMessagesOL.clear();
// Remove all listeners
for (final Object v : myMessageProviders) {
((IObservableValue) v).removeChangeListener(this);
}
final IObservableList decoratorMessages = getBinding().getDecorator().getMessages();
if (decoratorMessages != null) {
decoratorMessages.removeChangeListener(this);
}
myMessageProviders = null;
final ContextMessageDecorator contextMessageDecorator = myBinding.getContext().getService(
ContextMessageDecorator.class);
if (contextMessageDecorator != null) {
contextMessageDecorator.removeMessageProvider(this);
}
IManager.Factory.getManager().eAdapters().remove(this);
if (Activator.getDefault().TRACE_LIFECYCLE_VALUE_BINDING_MESSAGE_DECORATOR) {
LogUtils.debug(this, "dispose " + hashCode() + ": " + getBinding()); //$NON-NLS-1$ //$NON-NLS-2$
}
}
@Override
public void notifyChanged(Notification msg) {
if (msg.isTouch()) return;
/*
* React to change is the configuration used by this decorator.
*/
if ((msg.getFeature() == IUIBindingsPackage.Literals.MANAGER__MESSAGE_DECORATION_POSITION)
|| (msg.getFeature() == IUIBindingsPackage.Literals.MANAGER__ALTERNATIVE_DECORATION_POSITION)
|| (msg.getFeature() == IUIBindingsPackage.Literals.MANAGER__MESSAGE_DECORATION_MINIMUM_SEVERITY)
|| (msg.getFeature() == IUIBindingsPackage.Literals.MANAGER__REQUIRED_VB_IMAGE_DECORATION_SHOWN)
|| (msg.getFeature() == IUIBindingsPackage.Literals.MANAGER__ASSIST_VB_IMAGE_DECORATION_SHOWN)
|| (msg.getFeature() == IUIBindingsPackage.Literals.MANAGER__QUICKFIX_VB_IMAGE_DECORATION_SHOWN)) {
updateDecoration();
return;
}
/*
* When the state of the underlying binding changes to OK or DISPOSED, then react to this.
*/
if (msg.getFeature() == IUIBindingsPackage.Literals.BINDING__STATE) {
switch (getBinding().getState()) {
case OK:
init();
//$FALL-THROUGH$ fallthrough
case DISPOSED:
getBinding().eAdapters().remove(this);
break;
default:
break;
}
return;
}
}
@Override
public void handleEvent(Event event) {
updateDecoration();
}
/**
* Common change listener used to update the decoration whenever any of the individual elements
* of {@link #myMessageProviders} is changed.
*/
@Override
public void handleChange(ChangeEvent event) {
updateDecoration();
}
/**
* Common change listener used to update the decoration whenever changes are initiated for the
* UI Observable. See {@link TextObservableValue} for more information.
*/
@Override
public void handleDelayedChange(DelayedChangeEvent event) {
updateDecoration();
}
@Override
public IValueBinding getBinding() {
return myBinding;
}
/**
* Array with all the added message providers for this messages decorator.
* <p>
* The list elements are observable values that returns an {@link IBindingMessage} or
* <code>null</code>.
* <p>
* The array is static - once it is created, it does not change.
*/
private IObservableValue[] myMessageProviders = null;
/**
* The current list of outstanding messages for this decorator.
* <p>
* These messages comes from the outside. The party that adds a message must also remove it
* again.
*
* @see #addMessage(IBindingMessage)
* @see #removeMessage(IBindingMessage)
*/
private final List<IBindingMessage> myOutstandingMessages = new ArrayList<IBindingMessage>();
/**
* The current list of shown messages for this decorator.
* <p>
* The messages all have the same message type.
*/
private final List<IBindingMessage> myMessages = new ArrayList<IBindingMessage>();
@Override
public IObservableList getMessages() {
return myMessagesOL;
}
private final IObservableList myMessagesOL = new WritableList(myMessages, IBindingMessage.class);
@Override
public void addMessage(IBindingMessage message) {
if (Activator.getDefault().TRACE_LIFECYCLE_VALUE_BINDING_MESSAGE_DECORATOR) {
// LogUtils.DEBUG_STRACK_LEVELS = 10;
LogUtils.debug(this, hashCode() + ": " + getBinding() + "\n" + message + "@" + message.hashCode()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
myOutstandingMessages.add(message);
updateDecoration();
}
@Override
public void removeMessage(IBindingMessage message) {
if (Activator.getDefault().TRACE_LIFECYCLE_VALUE_BINDING_MESSAGE_DECORATOR) {
LogUtils.debug(this, hashCode() + ": " + getBinding() + "\n" + message + "@" + message.hashCode()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
myOutstandingMessages.remove(message);
updateDecoration();
}
/**
* Returns the current list of quick fixes..
* <p>
* A new list is returned for every call.
*
* @return the list
*/
public List<IQuickfixProposal> getQuickfixes() {
final IManager manager = IManager.Factory.getManager();
final List<IQuickfixProposal> quickfixes = new ArrayList<IQuickfixProposal>();
for (final IBindingMessage m : myMessages) {
manager.getQuickfixes(m, quickfixes);
}
return quickfixes;
}
/**
* Updates the message decoration of this decorator.
*/
protected void updateDecoration() {
if (getBinding().getState() != BindingState.OK) return;
IManagerRunnable.Factory.asyncExec("updateDecorations", this, new Runnable() {
@Override
public void run() {
updateDecorationDelayed();
}
});
}
/**
* The object observed by this decorator. Used to track changes in master-detail bindings.
*/
private EObject myObservedObject = null;
private Image myMessageDecorationImage;
private String myMessageDecorationMessage;
private Image myAlternativeDecorationImage;
private String myAlternativeDecorationMessage;
/**
* The VAM...
*/
public static final IValidatorAdapterManager VALIDATION_MANAGER = IValidatorAdapterManager.Factory.getManager();
/**
* Decoration used to display <em>a value is required</em> for the binding.
*/
public static final FieldDecoration REQUIRED_FIELD_DECORATOR = FieldDecorationRegistry.getDefault()
.getFieldDecoration(FieldDecorationRegistry.DEC_REQUIRED);
/**
* Decoration used to display <em>additional information</em> is available for the binding.
*/
public static final FieldDecoration INFORMATION_FIELD_DECORATOR = FieldDecorationRegistry.getDefault()
.getFieldDecoration(FieldDecorationRegistry.DEC_INFORMATION);
/**
* Decoration used to display <em>a warning</em> is detected for the binding.
*/
public static final FieldDecoration WARNING_FIELD_DECORATOR = FieldDecorationRegistry.getDefault()
.getFieldDecoration(FieldDecorationRegistry.DEC_WARNING);
/**
* Decoration used to display <em>an error</em> is detected for the binding.
*/
public static final FieldDecoration ERROR_FIELD_DECORATOR = FieldDecorationRegistry.getDefault()
.getFieldDecoration(FieldDecorationRegistry.DEC_ERROR);
/**
* Decoration used to display <em>a content proposal</em> is detected for the binding.
*/
public static final FieldDecoration CONTENT_PROPOSAL_FIELD_DECORATOR = FieldDecorationRegistry.getDefault()
.getFieldDecoration(FieldDecorationRegistry.DEC_CONTENT_PROPOSAL);
/**
* Decoration used to display <em>a quick fix proposal</em> is detected for the binding.
*/
public static final FieldDecoration QUICKFIX_FIELD_DECORATOR = FieldDecorationRegistry.getDefault()
.getFieldDecoration(FieldDecorationRegistry.DEC_ERROR_QUICKFIX);
/**
* @see #updateDecoration()
*/
protected void updateDecorationDelayed() {
if (getBinding().getState() != BindingState.OK) return;
/*
* As this operation is delayed, the widget might be disposed in the mean time...
*/
if (getBinding().getUIObservable().isDisposed()) return;
if (Activator.getDefault().TRACE_LIFECYCLE_VALUE_BINDING_MESSAGE_DECORATOR) {
LogUtils.debug(this, "update delayed " + hashCode() + ": " + getBinding()); //$NON-NLS-1$ //$NON-NLS-2$
}
if (VALIDATION_MANAGER != null) {
if (getBinding().getModelObservable().isDisposed()) {
LogUtils.debug(this, "value is disposed");
return;
}
/*
* Check if the observed object of the binding has changed. If so, then reset the
* decoration for the validation manager - this will remove all current messages and set
* up a new set...
*/
final EObject newObservedObject = getBinding().getModelObject();
if (myObservedObject != newObservedObject) {
myObservedObject = newObservedObject;
VALIDATION_MANAGER.resetDecorator(this);
}
}
/*
* Make a list of all the potential messages
*/
final List<IBindingMessage> ml = new ArrayList<IBindingMessage>();
final BindingMessageSeverity minimumSeverity = IManager.Factory.getManager()
.getMessageDecorationMinimumSeverity();
for (final Object v : myMessageProviders) {
final IBindingMessage message = (IBindingMessage) ((IObservableValue) v).getValue();
if (message.getSeverity().compareTo(minimumSeverity) < 0) {
continue;
}
ml.add(message);
}
final IObservableList decoratorMessages = getBinding().getDecorator().getMessages();
if (decoratorMessages != null) {
for (final Object m : decoratorMessages) {
final IBindingMessage message = (IBindingMessage) m;
if (message.getSeverity().compareTo(minimumSeverity) < 0) {
continue;
}
ml.add(message);
}
}
for (final IBindingMessage message : myOutstandingMessages) {
if (message.getSeverity().compareTo(minimumSeverity) < 0) {
continue;
}
ml.add(message);
}
/*
* Find the current max type and the combined messages to use...
*/
int maxType = IMessageProvider.NONE;
final List<IBindingMessage> topList = new ArrayList<IBindingMessage>();
for (final IBindingMessage message : ml) {
final int type = message.getMessageType();
if (type > maxType) {
topList.clear();
maxType = type;
}
if (type == maxType) {
topList.add(message);
}
}
/*
* Weed out any superseded messages.
*
* There has to be at least two messages before there any anything to weed out...
*/
if (topList.size() > 1) {
final IBindingMessage[] ma = topList.toArray(new IBindingMessage[topList.size()]);
for (int i = 0; i < ma.length; i++) {
final IBindingMessage a = ma[i];
for (int j = i + 1; j < ma.length; j++) {
final IBindingMessage b = ma[j];
if (a.supersedes(b)) {
topList.remove(b);
continue;
}
if (b.supersedes(a)) {
topList.remove(a);
break;
}
}
}
}
/*
* Updated the messages
*/
for (final IBindingMessage m : topList) {
if (myMessages.contains(m)) {
continue;
}
myMessagesOL.add(m);
}
for (final IBindingMessage m : myMessages.toArray(new IBindingMessage[myMessages.size()])) {
if (topList.contains(m)) {
continue;
}
myMessagesOL.remove(m);
}
final StringBuilder sb = new StringBuilder(100);
for (final IBindingMessage message : myMessages) {
if (sb.length() > 0) {
sb.append('\n');
}
sb.append(message.getMessage());
}
/*
* Show the appropriate message decorations
*/
final Image oldMessageDecorationImage = myMessageDecorationImage;
final String oldMessageDecorationMessage = myMessageDecorationMessage;
switch (maxType) {
case IMessageProvider.NONE:
myMessageDecorationImage = null;
break;
case IMessageProvider.ERROR:
myMessageDecorationImage = ERROR_FIELD_DECORATOR.getImage();
break;
case IMessageProvider.WARNING:
myMessageDecorationImage = WARNING_FIELD_DECORATOR.getImage();
break;
case IMessageProvider.INFORMATION:
myMessageDecorationImage = INFORMATION_FIELD_DECORATOR.getImage();
break;
default:
break;
}
myMessageDecorationMessage = sb.toString();
/*
* The alternative stuff is only relevant if the binding is changeable...
*/
final Image oldAlternativeDecorationImage = myAlternativeDecorationImage;
final String oldAlternativeDecorationMessage = myAlternativeDecorationMessage;
if (getBinding().isChangeable()) {
final IManager manager = IManager.Factory.getManager();
/*
* Only show the alternative decorations for controls if they have the focus - but not
* for checkboxes
*/
final Control control = getBinding().getControl();
boolean showAlternativeDecorations = control != null && control.isFocusControl();
if (control instanceof Button && (control.getStyle() & SWT.CHECK) == SWT.CHECK) {
showAlternativeDecorations = false;
}
// TODO TMTM add key bindings
if (getQuickfixes().size() > 0 && manager.isQuickfixVBImageDecorationShown()) {
myAlternativeDecorationImage = QUICKFIX_FIELD_DECORATOR.getImage();
myAlternativeDecorationMessage = QUICKFIX_FIELD_DECORATOR.getDescription();
} else if (showAlternativeDecorations && getBinding().getDataType().isRequired()
&& manager.isRequiredVBImageDecorationShown()) {
myAlternativeDecorationImage = REQUIRED_FIELD_DECORATOR.getImage();
myAlternativeDecorationMessage = Messages.ValueBindingMessageImageDecorator_ValueRequired;
} else if (showAlternativeDecorations && getBinding().getDecorator().getValidUIList() != null
&& getBinding().getUIAttribute().getFieldAssistAdapter() != null
&& manager.isAssistVBImageDecorationShown()) {
myAlternativeDecorationImage = CONTENT_PROPOSAL_FIELD_DECORATOR.getImage();
myAlternativeDecorationMessage = Messages.ValueBindingMessageImageDecorator_ContentAssistAvailanble;
} else {
myAlternativeDecorationImage = null;
myAlternativeDecorationMessage = null;
}
}
/*
* If everything is the same, then do nothing
*/
if (BasicUtils.equals(oldMessageDecorationImage, myMessageDecorationImage)
&& BasicUtils.equals(oldMessageDecorationMessage, myMessageDecorationMessage)
&& BasicUtils.equals(oldAlternativeDecorationImage, myAlternativeDecorationImage)
&& BasicUtils.equals(oldAlternativeDecorationMessage, myAlternativeDecorationMessage)) return;
/*
* Update the binding
*/
getBinding().updateBinding();
}
/**
* Updates the context based on the current images and message values.
*
* @param context the extender context to update
*/
public void extend(IUIBindingDecoratorExtenderContext context) {
if (myMessageDecorationImage != null) {
// LogUtils.debug(this, myMessageDecorationMessage);
context.setDecoratingImage(IManager.Factory.getManager().getMessageDecorationPosition(), true,
myMessageDecorationImage, myMessageDecorationMessage);
}
if (myAlternativeDecorationImage != null) {
// LogUtils.debug(this, myAlternativeDecorationMessage);
context.setDecoratingImage(IManager.Factory.getManager().getAlternativeDecorationPosition(), true,
myAlternativeDecorationImage, myAlternativeDecorationMessage);
}
}
@Override
public boolean accept(IBindingMessage unboundMessage) {
if (myObservedObject == null) return false;
final IValueBinding binding = getBinding();
final IObservable modelObservable = binding.getModelObservable();
Object key = null;
if (modelObservable instanceof IKeyedObservable) {
key = ((IKeyedObservable) modelObservable).getObservableKey();
}
FeatureMatchingAlgorithm matchingAlgorithm;
if (binding.getArgument(Constants.ARG_MODEL_OBJECT_MESSAGES, Boolean.class, Boolean.FALSE) == Boolean.TRUE) {
matchingAlgorithm = FeatureMatchingAlgorithm.EXACT_OR_NULL;
} else {
matchingAlgorithm = FeatureMatchingAlgorithm.EXACT;
}
if (unboundMessage.matches(myObservedObject, binding.getModelFeature(), key, matchingAlgorithm)) return true;
if (binding.getArgument(Constants.ARG_VALUE_OBJECT_MESSAGES, Boolean.class, Boolean.FALSE) == Boolean.TRUE
&& unboundMessage.matches(myObservedObject, null, null, FeatureMatchingAlgorithm.EXACT)) return true;
return false;
}
}