// $codepro.audit.disable numericLiterals /******************************************************************************* * 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; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import org.eclipse.core.databinding.observable.list.IObservableList; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.Status; import org.eclipse.emf.databinding.EObjectObservableList; import org.eclipse.emf.ecore.EAttribute; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EClassifier; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.EcorePackage; import org.eclipse.emf.ecore.util.EObjectEList; import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain; import org.eclipse.emf.edit.domain.EditingDomain; import org.eclipse.emf.edit.provider.ReflectiveItemProviderAdapterFactory; import org.eclipse.jface.resource.ImageRegistry; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import com.rcpcompany.uibindings.internal.Activator; import com.rcpcompany.uibindings.internal.ChildCreationSpecification; import com.rcpcompany.uibindings.internal.decorators.GenericEObjectDecorator; import com.rcpcompany.uibindings.internal.observables.MyDetailObservableList; import com.rcpcompany.uibindings.model.utils.BasicUtils; import com.rcpcompany.uibindings.model.utils.EcoreExtendedUtils; import com.rcpcompany.uibindings.observables.IObservableListMapper; import com.rcpcompany.uibindings.observables.ProxyObservableValue; import com.rcpcompany.uibindings.utils.CoreRuntimeException; import com.rcpcompany.uibindings.utils.ExtendedCommandStack; import com.rcpcompany.utils.basic.ClassUtils; import com.rcpcompany.utils.logging.LogUtils; /** * Various utility methods. * <p> * Contrary to the utility methods found in {@link BasicUtils}, these somehow depends on UI * Bindings, or currently only makes sense in that framework. * * @author Tonny Madsen, The RCP Company */ public final class UIBindingsUtils { private UIBindingsUtils() { } /** * Adds to the specified list of {@link IChildCreationSpecification} with the specified * information. * <p> * Also adds any sub-classes of child type. * * @param specs the list of specifications to add to * @param parent the parent object * @param ref the reference * @param childType the child type * @param index TODO */ public static void addToChildCreationSpecification(final List<IChildCreationSpecification> specs, EObject parent, EReference ref, final EClass childType, int index) { /* * Allow the user to prevent the addition */ final IBindingDataType dt = IBindingDataType.Factory.create(parent, ref); if (!childType.isAbstract() && !childType.isInterface() && dt.getArgument(Constants.ARG_NEW_ALLOWED, null, Boolean.class, Boolean.TRUE)) { specs.add(new ChildCreationSpecification(parent, ref, childType, index)); } final Collection<EClass> subClasses = EcoreExtendedUtils.getSubClasses(childType); if (subClasses == null) return; for (final EClass c : subClasses) { addToChildCreationSpecification(specs, parent, ref, c, index); } } /** * Returns {@link IChildCreationSpecification} for children of the specified list. * * @param l the list * @param sibling a possible sibling, if non-<code>null</code> * * @return the possible top-level children */ public static List<IChildCreationSpecification> getPossibleTopLevelChildObjects(IObservableList l, EObject sibling) { final List<IChildCreationSpecification> specs = new ArrayList<IChildCreationSpecification>(); /* * Figure out the EReference of the elements */ EObject parent = null; EReference ref = null; if (l instanceof EObjectEList<?>) { final EObjectEList<?> el = (EObjectEList<?>) l; final EStructuralFeature sf = el.getEStructuralFeature(); if (sf instanceof EReference) { parent = (EObject) el.getNotifier(); ref = (EReference) sf; } } if (ref == null && l instanceof MyDetailObservableList) { parent = (EObject) ((MyDetailObservableList) l).getObserved(); ref = (EReference) ((MyDetailObservableList) l).getElementType(); } if (ref == null && l instanceof EObjectObservableList) { parent = (EObject) ((EObjectObservableList) l).getObserved(); ref = (EReference) ((EObjectObservableList) l).getElementType(); } if (ref == null) return specs; final EClass childType = ref.getEReferenceType(); int index = -1; if (sibling != null) { index = l.indexOf(sibling); if (index == -1) return specs; } UIBindingsUtils.addToChildCreationSpecification(specs, parent, ref, childType, index); return specs; } /** * Creates a new validation error status with the given message. * <p> * This error message does <em>not</em> prevent the data binding to set the value * * @param isFatal <code>true</code> if this is a fatal error - value should not set in data * binding * @param code the code used for the message * @param m the message for the error * @return a new error status with the given message */ public static IStatus error(boolean isFatal, int code, String m) { if (isFatal) return new Status(IStatus.ERROR, Activator.ID, code, m, null); else return new MyNonFatalStatus(IStatus.ERROR, Activator.ID, code, m, null); } /** * Creates a new validation error status with the given message. * <p> * This error message does <em>not</em> prevent the data binding to set the value * * @param code the code used for the message * @param m the message for the error * @return a new error status with the given message */ public static IStatus error(int code, String m) { return error(IManager.Factory.getManager().isValidationErrorsAreFatal(), code, m); } /** * Creates a new validation error status with the given message. * <p> * This error message does <em>not</em> prevent the data binding to set the value * * @param isFatal <code>true</code> if this is a fatal error - value should not set in data * binding * @param code the code used for the message * @param m the message for the error * * @return a new error status with the given message */ public static void throwError(boolean isFatal, int code, String m) { throw new CoreRuntimeException(error(isFatal, code, m)); } /** * Creates a new validation warning status with the given message. * <p> * This warning message does <em>not</em> prevent the data binding to set the value * * @param code the code used for the message * @param m the message for the warning * @return a new warning status with the given message */ public static IStatus warning(int code, String m) { return new Status(IStatus.WARNING, Activator.ID, code, m, null); } /** * Returns whether the specified status is a fatal error or not. * <p> * A fatal error will not result in the value being set in a data binding, but wil still show as * an error. * * @param status the status to check * @return <code>true</code> if it is a fatal error, and <code>false</code> otherwise */ public static boolean isFatalError(IStatus status) { /* * Assuming multi-status have the correct severity... */ if (status.getSeverity() != IStatus.ERROR) return false; final IStatus[] children = status.getChildren(); if (children != null) { for (final IStatus c : children) { if (isFatalError(c)) return true; } } if (status instanceof MultiStatus) return false; if (status instanceof MyNonFatalStatus) return false; return true; } /** * The default alignment for specific types. */ private static final Map<Object, Integer> DEFAULT_ALIGNMENT = new HashMap<Object, Integer>(); static { DEFAULT_ALIGNMENT.put(EcorePackage.Literals.EBYTE, SWT.RIGHT); DEFAULT_ALIGNMENT.put(EcorePackage.Literals.EBYTE_OBJECT, SWT.RIGHT); DEFAULT_ALIGNMENT.put(EcorePackage.Literals.ESHORT, SWT.RIGHT); DEFAULT_ALIGNMENT.put(EcorePackage.Literals.ESHORT_OBJECT, SWT.RIGHT); DEFAULT_ALIGNMENT.put(EcorePackage.Literals.EINT, SWT.RIGHT); DEFAULT_ALIGNMENT.put(EcorePackage.Literals.EINTEGER_OBJECT, SWT.RIGHT); DEFAULT_ALIGNMENT.put(EcorePackage.Literals.EFLOAT, SWT.RIGHT); DEFAULT_ALIGNMENT.put(EcorePackage.Literals.EFLOAT_OBJECT, SWT.RIGHT); DEFAULT_ALIGNMENT.put(EcorePackage.Literals.EDOUBLE, SWT.RIGHT); DEFAULT_ALIGNMENT.put(EcorePackage.Literals.EDOUBLE_OBJECT, SWT.RIGHT); } /** * Returns the default alignment for the specific value type. * <p> * If you want to change the alignment use the <code>uibindings/model</code> and * <code>uibindings/model/feature elements</code>. * * @param valueType the value type to test * @return the default alignment */ public static int defaultAlignment(Object valueType) { if (valueType instanceof EStructuralFeature) { final EStructuralFeature sf = (EStructuralFeature) valueType; valueType = sf.getEType(); } final Integer alignment = DEFAULT_ALIGNMENT.get(valueType); if (alignment != null) return alignment; return SWT.NONE; } /** * Returns a corner image for the specified position and color. * <p> * The returned image is cached and may not be disposed. * * @param position the wanted position of the image * @param rgb the wanted color * @return the image */ public static Image getCornerImage(DecorationPosition position, RGB rgb) { final String key = "CORNER_IMAGE:" + position + ":" + rgb; //$NON-NLS-1$ //$NON-NLS-2$ final ImageRegistry ir = JFaceResources.getImageRegistry(); Image image = ir.get(key); if (image != null) return image; final Display device = Display.getDefault(); final Color color = new Color(device, rgb); image = new Image(device, 5, 5); final GC gc = new GC(image); gc.setBackground(color); switch (position) { case TOP_LEFT: gc.fillPolygon(new int[] { 0, 0, 4, 0, 0, 4 }); break; case CENTER_LEFT: gc.fillPolygon(new int[] { 0, 0, 4, 2, 0, 4 }); break; case BOTTOM_LEFT: gc.fillPolygon(new int[] { 0, 0, 4, 4, 0, 4 }); break; case TOP_RIGHT: gc.fillPolygon(new int[] { 4, 0, 0, 0, 4, 4 }); break; case CENTER_RIGHT: gc.fillPolygon(new int[] { 4, 0, 2, 0, 4, 4 }); break; case BOTTOM_RIGHT: gc.fillPolygon(new int[] { 4, 0, 4, 0, 4, 4 }); break; } gc.dispose(); color.dispose(); /* * Set the transparent color */ final ImageData ideaData = image.getImageData(); final int whitePixel = ideaData.palette.getPixel(new RGB(255, 255, 255)); ideaData.transparentPixel = whitePixel; ir.put(key, image); return image; } /** * Returns a square image for the specified color. * <p> * The returned image is cached and may not be disposed. * * @param rgb the wanted color * @param size TODO * @return the image */ public static Image getSquareImage(RGB rgb, int size) { final String key = "SQUARE_IMAGE:" + rgb; //$NON-NLS-1$ final ImageRegistry ir = JFaceResources.getImageRegistry(); Image image = ir.get(key); if (image != null) return image; final Display device = Display.getDefault(); final Color color = new Color(device, rgb); image = new Image(device, size, size); final GC gc = new GC(image); gc.setBackground(color); gc.fillRectangle(0, 0, size, size); gc.dispose(); color.dispose(); ir.put(key, image); return image; } protected static class MyNonFatalStatus extends Status { protected MyNonFatalStatus(int severity, String pluginId, int code, String message, Throwable exception) { super(severity, pluginId, code, message, exception); } } /** * Constructs and returns a new simple editing domain suitable for UI Bindings. * * @return the new editing domain */ public static EditingDomain createEditingDomain() { final EditingDomain newDomain = new AdapterFactoryEditingDomain(new ReflectiveItemProviderAdapterFactory(), new ExtendedCommandStack()); return newDomain; } private static Boolean myIsWindows = null; /** * Returns whether the application is running under Windows (any version). * * @return <code>true</code> if Windows, <code>false</code> otherwise */ public static boolean isWindows() { if (myIsWindows == null) { final String osname = System.getProperty("os.name"); //$NON-NLS-1$ myIsWindows = osname.startsWith("Windows"); //$NON-NLS-1$ } return myIsWindows; } private static Boolean myIsWindowsXP = null; /** * Returns whether the application is running under Windows XP. * * @return <code>true</code> if Windows XP, <code>false</code> otherwise */ public static boolean isWindowsXP() { if (myIsWindowsXP == null) { final String osname = System.getProperty("os.name"); //$NON-NLS-1$ final String osversion = System.getProperty("os.version"); //$NON-NLS-1$ //LogUtils.debug(Activator.getDefault(), "name='" + osname + "', version='" + osversion + "'"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ myIsWindowsXP = osname.startsWith("Windows") && "5.1".equals(osversion); //$NON-NLS-1$ //$NON-NLS-2$ } return myIsWindowsXP; } /** * Interface used to map an {@link EObject} to the identifier attribute of the object. * <p> * An implementation can also implement {@link IDisposable}. * * @see GenericEObjectDecorator * @author Tonny Madsen, The RCP Company */ public interface IClassIdentiferMapper extends IObservableListMapper { /** * @see IUIBindingDecorator#getDisplayObservableValue(IObservableValue) * @param value the value * @param editingDomain the editing domain used for any changes in the returned observable * value * @return the observable value for the displayed text */ IObservableValue getObservableValue(IObservableValue value, EditingDomain editingDomain); } /** * Creates a list mapper that corresponding to the facilities of the specified type. * <p> * The mapper is chosen according to the following priorities: * <ul> * <li>annotated field</li> * <li>via genmodel label field - requires additional generation of information</li> * <li>by field name: label, name, fullName</li> * <li>by field type: String</li> * <li>{@link #toString()}</li> * </ul> * * @param binding the binding to map * @param ec the class to create the mapper for * * @return the mapper */ public static IClassIdentiferMapper createClassIdentiferMapper(IBinding binding, EClass ec) { EStructuralFeature feature; /* * Any constant string is used first of all... * * TODO: Problem: if text is specified, icon and other arguments are ignored! */ final String constantText = binding.getArgument(Constants.ARG_TEXT, String.class, null); if (constantText != null) return new IClassIdentiferMapper() { @Override public Object map(Object value) { return constantText; } @Override public IObservableValue getObservableValue(IObservableValue value, EditingDomain editingDomain) { return value; } }; // Via Annotation final String featureNames = binding.getArgument(Constants.ARG_FEATURE_NAME, String.class, null); if (featureNames != null) { // Pattern.compile("(\\p{L}+)") final StringTokenizer st = new StringTokenizer(featureNames, "."); EClassifier c = ec; final List<EStructuralFeature> sfs = new ArrayList<EStructuralFeature>(); while (st.hasMoreTokens()) { final String name = st.nextToken(); binding.assertTrue(c instanceof EClass, "Intermidiate features must be references"); final EClass eClass = (EClass) c; feature = eClass.getEStructuralFeature(name); binding.assertTrue(feature != null, "Unknown feature: '" + eClass.getName() + "." + name + "' in argument " + Constants.ARG_FEATURE_NAME + " (" + featureNames + ")"); sfs.add(feature); c = feature.getEType(); } switch (sfs.size()) { case 0: LogUtils.error(binding, "Feature names '" + featureNames + "' does not exist. Ignored.", binding.getCreationPoint()); break; case 1: return new SingleFeatureMapper(sfs.get(0)); default: return new MultipleFeatureMapper(sfs.toArray(new EStructuralFeature[sfs.size()])); } } // By Field Name feature = ec.getEStructuralFeature("label"); if (feature != null) return new SingleFeatureMapper(feature); feature = ec.getEStructuralFeature("name"); if (feature != null) return new SingleFeatureMapper(feature); feature = ec.getEStructuralFeature("fullName"); if (feature != null) return new SingleFeatureMapper(feature); // By Field Type for (final EAttribute a : ec.getEAllAttributes()) { if (a.getEType() == EcorePackage.Literals.ESTRING) return new SingleFeatureMapper(a); } // Fall back on toString().... return DEFAULT_MAPPER; } /** * The singleton default mapper. */ public static final DefaultMapper DEFAULT_MAPPER = new DefaultMapper(); /** * {@link IClassIdentiferMapper} based on {@link Object#toString()}. */ public static class DefaultMapper implements IClassIdentiferMapper { @Override public Object map(Object value) { return value != null ? value.toString() : ""; } @Override public IObservableValue getObservableValue(IObservableValue value, EditingDomain editingDomain) { return value; } @Override public String toString() { return ClassUtils.getLastClassName(this) + "[]"; } } /** * {@link IClassIdentiferMapper} based on a single {@link EStructuralFeature feature}. */ protected static class SingleFeatureMapper implements IClassIdentiferMapper { private final EStructuralFeature myFeature; private IObservableValue ov = null; /** * Constructs and returns a new mapper based on the specified feature. * * @param feature the feature of the mapper */ protected SingleFeatureMapper(EStructuralFeature feature) { myFeature = feature; } @Override public Object map(Object value) { if (value != null) { value = ((EObject) value).eGet(myFeature); } if (value == null) { // value = ""; } return value; } @Override public IObservableValue getObservableValue(IObservableValue value, EditingDomain editingDomain) { if (ov == null) { ov = new ProxyObservableValue(value); ov = UIBindingsEMFObservables.observeDetailValue(value.getRealm(), editingDomain, ov, myFeature); } return ov; } @Override public String toString() { return ClassUtils.getLastClassName(this) + "[" + myFeature.getName() + "]"; } } /** * {@link IClassIdentiferMapper} based on a chain of {@link EStructuralFeature features}. */ protected static class MultipleFeatureMapper implements IClassIdentiferMapper, IDisposable { private final EStructuralFeature[] myFeatures; private IObservableValue ov = null; /** * Constructs and returns a new mapper based on the specified features. * * @param features the features of the mapper */ protected MultipleFeatureMapper(EStructuralFeature[] features) { myFeatures = features; } @Override public Object map(Object value) { for (final EStructuralFeature sf : myFeatures) { if (value == null) return value; value = ((EObject) value).eGet(sf); } if (value == null) return value; return value; } @Override public IObservableValue getObservableValue(IObservableValue value, EditingDomain editingDomain) { if (ov == null) { ov = value; for (final EStructuralFeature sf : myFeatures) { if (ov == null) return null; ov = UIBindingsEMFObservables.observeDetailValue(ov.getRealm(), editingDomain, ov, sf); } } return ov; } @Override public void dispose() { if (ov != null) { ov.dispose(); } } @Override public String toString() { final StringBuilder sb = new StringBuilder(50); sb.append(ClassUtils.getLastClassName(this)).append('['); boolean first = true; for (final EStructuralFeature sf : myFeatures) { if (!first) { sb.append('.'); } sb.append(sf.getName()); first = false; } sb.append(']'); return sb.toString(); } } /** * Mapping of boxed to primitive data types - e.g. <code>Integer</code> to <code>int</code>. */ private static final Map<String, String> BOXED2PRIMITIVE = new HashMap<String, String>(); static { UIBindingsUtils.BOXED2PRIMITIVE.put(Boolean.class.getName(), Boolean.TYPE.getName()); UIBindingsUtils.BOXED2PRIMITIVE.put(Character.class.getName(), Character.TYPE.getName()); UIBindingsUtils.BOXED2PRIMITIVE.put(Byte.class.getName(), Byte.TYPE.getName()); UIBindingsUtils.BOXED2PRIMITIVE.put(Short.class.getName(), Short.TYPE.getName()); UIBindingsUtils.BOXED2PRIMITIVE.put(Integer.class.getName(), Integer.TYPE.getName()); UIBindingsUtils.BOXED2PRIMITIVE.put(Long.class.getName(), Long.TYPE.getName()); UIBindingsUtils.BOXED2PRIMITIVE.put(Float.class.getName(), Float.TYPE.getName()); UIBindingsUtils.BOXED2PRIMITIVE.put(Double.class.getName(), Double.TYPE.getName()); } /** * Returns the primitive data type corresponding to the specified boxed data type, if one * exists. * * @param boxed the boxed type * @return the primitive type or <code>null</code> */ public static String getBoxed2Primitive(String boxed) { return BOXED2PRIMITIVE.get(boxed); } /** * Calculates the needed Y adjustment for the specified control for decorations inside the * control. * <p> * Needed for {@link Tree} under Windows. * * @param control the control to calculate the adjustment for * @return the adjustment in pixels */ public static int calculateYAdjustment(final Control control) { int adjustY = 0; if (isWindows() && control instanceof Tree) { /* * BUG under Windows? * * Tree.getClientArea does not return the visible area of the tree, but rather the * complete area of the tree if there was "room" enough. * * The fix is to calculate the y of the top-index and correct with this when saving * locations inside a Tree. * * This code finds the bounds of the first root element. When this is not the top * element in the view of the tree, the bounds still returned with a negative y... */ final Tree t = (Tree) control; if (t.getItemCount() == 0) return 0; final TreeItem rootItem = t.getItem(0); adjustY = -rootItem.getBounds().y; // LogUtils.debug(control, "Adjust y " + adjustY + " for " + rootItem.getData()); } return adjustY; } }