diff options
author | Christian W. Damus | 2019-05-15 15:59:52 +0000 |
---|---|---|
committer | Christian Damus | 2019-05-16 16:57:55 +0000 |
commit | ec01e683ad249b8e6aeab6e29f86a420a3fab547 (patch) | |
tree | f0ebaab72cdc1b9cdd6ea110b0cdaca122a74c48 | |
parent | df8d884e8ee5485690d049f6a0a3d6d8e8ce024b (diff) | |
download | org.eclipse.emf.ecp.core-ec01e683ad249b8e6aeab6e29f86a420a3fab547.tar.gz org.eclipse.emf.ecp.core-ec01e683ad249b8e6aeab6e29f86a420a3fab547.tar.xz org.eclipse.emf.ecp.core-ec01e683ad249b8e6aeab6e29f86a420a3fab547.zip |
Bug 547271 - [Performance] Filtering and adding objects to large tables is slow
Provide a more efficient filtering mechanism and avoid redundant
viewer refreshes.
Cache a generated view model for each EClass that needs it and clone
in the usual way (plus tweaks for read-only controls) for each object
of that class.
Don’t set properties of cells that don’t need updating, because it is
expensive to do so (especially fg/bg colours and text).
Change-Id: I47a3fd894e3e692252fc4448806dc827ad2ab671
Signed-off-by: Christian W. Damus <give.a.damus@gmail.com>
12 files changed, 770 insertions, 208 deletions
diff --git a/bundles/org.eclipse.emf.ecp.edit.swt/src/org/eclipse/emf/ecp/edit/spi/swt/table/ECPFilterableCell.java b/bundles/org.eclipse.emf.ecp.edit.swt/src/org/eclipse/emf/ecp/edit/spi/swt/table/ECPFilterableCell.java new file mode 100644 index 0000000000..947985ac76 --- /dev/null +++ b/bundles/org.eclipse.emf.ecp.edit.swt/src/org/eclipse/emf/ecp/edit/spi/swt/table/ECPFilterableCell.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2019 Christian W. Damus and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christian W. Damus - initial API and implementation + ******************************************************************************/ +package org.eclipse.emf.ecp.edit.spi.swt.table; + +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.jface.viewers.CellLabelProvider; + +/** + * Optional interface (either implemented or provided as an {@linkplain IAdaptable adapter}) + * for a cell that supports filtering by its text content. This allows for complex cell + * renderers, for example, to avoid costly updates via the + * {@link CellLabelProvider#update(org.eclipse.jface.viewers.ViewerCell)} API that do more + * than just render text. + * + * @since 1.21 + */ +public interface ECPFilterableCell { + + /** + * Query the text to filter on. + * + * @param object the object to be filtered + * @return the text to filter on, or an empty string if none + */ + String getFilterableText(Object object); + +} diff --git a/bundles/org.eclipse.emf.ecp.view.model.generator/src/org/eclipse/emf/ecp/view/model/generator/ViewCache.java b/bundles/org.eclipse.emf.ecp.view.model.generator/src/org/eclipse/emf/ecp/view/model/generator/ViewCache.java new file mode 100644 index 0000000000..0ee09cc318 --- /dev/null +++ b/bundles/org.eclipse.emf.ecp.view.model.generator/src/org/eclipse/emf/ecp/view/model/generator/ViewCache.java @@ -0,0 +1,325 @@ +/******************************************************************************* + * Copyright (c) 2011-2019 EclipseSource Muenchen GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Edgar - initial API and implementation + * Christian W. Damus - bug 547271 + ******************************************************************************/ +package org.eclipse.emf.ecp.view.model.generator; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Random; +import java.util.function.Predicate; + +import org.eclipse.emf.ecore.EAttribute; +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.EReference; +import org.eclipse.emf.ecore.EStructuralFeature; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.emf.ecp.view.model.common.edit.provider.CustomReflectiveItemProviderAdapterFactory; +import org.eclipse.emf.ecp.view.spi.model.VControl; +import org.eclipse.emf.ecp.view.spi.model.VDomainModelReference; +import org.eclipse.emf.ecp.view.spi.model.VFeaturePathDomainModelReference; +import org.eclipse.emf.ecp.view.spi.model.VView; +import org.eclipse.emf.ecp.view.spi.model.VViewFactory; +import org.eclipse.emf.ecp.view.spi.model.VViewPackage; +import org.eclipse.emf.ecp.view.spi.table.model.VTableDomainModelReference; +import org.eclipse.emf.ecp.view.spi.table.model.VTableFactory; +import org.eclipse.emf.edit.provider.AdapterFactoryItemDelegator; +import org.eclipse.emf.edit.provider.ComposedAdapterFactory; +import org.eclipse.emf.edit.provider.IItemPropertyDescriptor; + +/** + * A cache of the generated {@link VView} for an {@code EObject}. + * It is assumed that the valid features of an {@link EClass} are the same for all instances + * of that class and will not change during its lifetime, in keeping with standard EMF + * assumptions and the actual behaviour of generated models. + */ +final class ViewCache { + + private static final int CACHE_SIZE = 100; + private static final Random RANDOMIZER = new Random(); + + @SuppressWarnings("serial") + private final Map<EClass, ViewRecord> views = new LinkedHashMap<EClass, ViewRecord>() { + @Override + protected boolean removeEldestEntry(java.util.Map.Entry<EClass, ViewRecord> eldest) { + return size() > CACHE_SIZE; + } + }; + private final AdapterFactoryItemDelegator delegator; + + /** + * Initializes me. + */ + ViewCache() { + super(); + + final ComposedAdapterFactory composedAdapterFactory = new ComposedAdapterFactory( + ComposedAdapterFactory.Descriptor.Registry.INSTANCE); + composedAdapterFactory.insertAdapterFactory(new CustomReflectiveItemProviderAdapterFactory()); + + delegator = new AdapterFactoryItemDelegator(composedAdapterFactory); + } + + /** + * Get the generated view for an {@code object}. + * + * @param object an object for which to get the generated mutable view model + * @return the generated view model for the {@code object} + */ + VView getView(EObject object) { + final EClass eClass = object.eClass(); + ViewRecord prototype = views.get(eClass); + if (prototype == null) { + prototype = generatePrototype(eClass); + views.put(eClass, prototype); + } + + final VView result = prototype.instantiate(object); + return result; + } + + private ViewRecord generatePrototype(EClass eClass) { + final VView view = VViewFactory.eINSTANCE.createView(); + view.setUuid(generateId(eClass, null)); + final ViewRecord result = new ViewRecord(view); + + final EObject example = EcoreUtil.create(eClass); + final Predicate<EStructuralFeature> isValidFeature = feature -> isValidFeature(feature, example); + eClass.getEAllStructuralFeatures().stream().filter(isValidFeature) + .forEach(feature -> { + final VControl control; + if (isTableFeature(feature)) { + control = VTableFactory.eINSTANCE.createTableControl(); + final VTableDomainModelReference tableDmr = VTableFactory.eINSTANCE + .createTableDomainModelReference(); + tableDmr.setDomainModelReference(createModelReference(feature)); + control.setDomainModelReference(tableDmr); + } else { + control = VViewFactory.eINSTANCE.createControl(); + control.setDomainModelReference(createModelReference(feature)); + } + control.setUuid(result.generateID(eClass, feature)); + view.getChildren().add(control); + + // If it was valid, then it had a property descriptor + final IItemPropertyDescriptor propertyDescriptor = delegator.getPropertyDescriptor(example, feature); + result.add(control, feature, propertyDescriptor); + }); + + // Let the adapter factory not leak the example instance + example.eAdapters().clear(); + + view.setRootEClass(eClass); + + return result; + } + + private VDomainModelReference createModelReference(final EStructuralFeature feature) { + final VFeaturePathDomainModelReference modelReference = VViewFactory.eINSTANCE + .createFeaturePathDomainModelReference(); + modelReference.setDomainModelEFeature(feature); + return modelReference; + } + + private boolean isTableFeature(EStructuralFeature feature) { + if (feature instanceof EReference) { + final EReference ref = (EReference) feature; + return ref.isMany() && ref.isContainment(); + } + return false; + } + + private boolean isValidFeature(EStructuralFeature feature, EObject owner) { + boolean result = !isInvalidFeature(feature); + + if (result) { + // Further, check that there's a property descriptor + result = delegator.getPropertyDescriptor(owner, feature) != null; + } + + return result; + } + + private boolean isInvalidFeature(EStructuralFeature feature) { + return isContainerReference(feature) || isTransient(feature) || isVolatile(feature); + } + + private boolean isContainerReference(EStructuralFeature feature) { + if (feature instanceof EReference) { + final EReference reference = (EReference) feature; + if (reference.isContainer()) { + return true; + } + } + + return false; + } + + private boolean isTransient(EStructuralFeature feature) { + return feature.isTransient(); + } + + private boolean isVolatile(EStructuralFeature feature) { + return feature.isVolatile(); + } + + // this is not unique, because of the use of hashCode, so it needs to be post-processed + private static String generateId(EClass eClass, EStructuralFeature feature) { + final StringBuilder stringBuilder = new StringBuilder(); + final EPackage ePackage = eClass.getEPackage(); + if (ePackage != null) { + /* might be null with dynamic emf */ + stringBuilder.append(ePackage.getNsURI()); + } + stringBuilder.append("#"); //$NON-NLS-1$ + stringBuilder.append(eClass.getName()); + if (feature != null) { + stringBuilder.append("#"); //$NON-NLS-1$ + stringBuilder.append(feature.getName()); + } + return Integer.toHexString(stringBuilder.toString().hashCode()); + } + + // + // Nested types + // + + /** + * Internal tracking of a generated view model with metadata for calculation + * of enablement of individual controls according to the EMF.Edit property + * descriptor for each control as driven by a particular object in the editor. + */ + private static final class ViewRecord { + private final Map<String, ControlRecord> controls = new HashMap<>(); + private final VView view; + + private final ViewCopier copier = new ViewCopier(); + + ViewRecord(VView view) { + super(); + + this.view = view; + } + + /** + * Generate an ID that is guaranteed to be unique within my view. + * + * @param eClass the owner class for which to generate the ID + * @param feature the feature for which to generate the ID + * + * @return the unique generated ID + */ + String generateID(EClass eClass, EStructuralFeature feature) { + String result = ViewCache.generateId(eClass, feature); + + while (controls.containsKey(result)) { + // mangle it + int value = Integer.parseInt(result, 16); + value = value ^ RANDOMIZER.nextInt(); + result = Integer.toHexString(value); + } + + return result; + } + + void add(VControl control, EStructuralFeature feature, IItemPropertyDescriptor propertyDescriptor) { + controls.put(control.getUuid(), new ControlRecord(feature, propertyDescriptor)); + } + + VView instantiate(EObject object) { + copier.setOwner(object); + final VView result = (VView) copier.copy(view); + copier.copyReferences(); + copier.clear(); + return result; + } + + // + // Nested types + // + + /** + * A specialized copier that sets control enablement computed from + * the EMF.Edit property source for the control. + */ + @SuppressWarnings("serial") + private final class ViewCopier extends EcoreUtil.Copier { + + private EObject owner; + + ViewCopier() { + super(); + } + + @Override + public void clear() { + owner = null; + super.clear(); + } + + @Override + protected void copyAttribute(EAttribute eAttribute, EObject eObject, EObject copyEObject) { + // Don't copy the read-only attribute; we calculate it + if (eAttribute != VViewPackage.Literals.ELEMENT__READONLY) { + super.copyAttribute(eAttribute, eObject, copyEObject); + } + + if (eAttribute == VViewPackage.Literals.ELEMENT__UUID) { + // We now have the UUID, so can compute enablement override + if (copyEObject instanceof VControl) { + final VControl control = (VControl) copyEObject; + final ControlRecord record = controls.get(control.getUuid()); + if (record != null) { + control.setReadonly(!record.isEditable(owner)); + } + } + } + } + + /** + * Set the object for which we are copying the view model, to edit it. + * + * @param owner the owner object of the features to be edited + */ + void setOwner(EObject owner) { + this.owner = owner; + } + } + + } + + /** + * A record tracking metadata about the structural feature edited by a control, + * in particular for determination of enablement according to its EMF.Edit item + * provider. + */ + private static final class ControlRecord { + private final IItemPropertyDescriptor propertyDescriptor; + private final boolean changeable; + + ControlRecord(EStructuralFeature feature, IItemPropertyDescriptor propertyDescriptor) { + super(); + + this.propertyDescriptor = propertyDescriptor; + changeable = feature.isChangeable(); + } + + boolean isEditable(EObject owner) { + return changeable && propertyDescriptor.canSetProperty(owner); + } + } + +} diff --git a/bundles/org.eclipse.emf.ecp.view.model.generator/src/org/eclipse/emf/ecp/view/model/generator/ViewProvider.java b/bundles/org.eclipse.emf.ecp.view.model.generator/src/org/eclipse/emf/ecp/view/model/generator/ViewProvider.java index cc019c3795..ad654543ac 100644 --- a/bundles/org.eclipse.emf.ecp.view.model.generator/src/org/eclipse/emf/ecp/view/model/generator/ViewProvider.java +++ b/bundles/org.eclipse.emf.ecp.view.model.generator/src/org/eclipse/emf/ecp/view/model/generator/ViewProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2011-2018 EclipseSource Muenchen GmbH and others. + * Copyright (c) 2011-2019 EclipseSource Muenchen GmbH and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -10,153 +10,35 @@ * * Contributors: * Edgar - initial API and implementation + * Christian W. Damus - bug 547271 ******************************************************************************/ package org.eclipse.emf.ecp.view.model.generator; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.Set; - -import org.eclipse.emf.common.notify.AdapterFactory; -import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EObject; -import org.eclipse.emf.ecore.EPackage; -import org.eclipse.emf.ecore.EReference; -import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.util.EcoreUtil; -import org.eclipse.emf.ecp.view.model.common.edit.provider.CustomReflectiveItemProviderAdapterFactory; -import org.eclipse.emf.ecp.view.spi.model.VControl; -import org.eclipse.emf.ecp.view.spi.model.VDomainModelReference; -import org.eclipse.emf.ecp.view.spi.model.VFeaturePathDomainModelReference; import org.eclipse.emf.ecp.view.spi.model.VView; -import org.eclipse.emf.ecp.view.spi.model.VViewFactory; import org.eclipse.emf.ecp.view.spi.model.VViewModelProperties; import org.eclipse.emf.ecp.view.spi.provider.IViewProvider; -import org.eclipse.emf.ecp.view.spi.table.model.VTableDomainModelReference; -import org.eclipse.emf.ecp.view.spi.table.model.VTableFactory; -import org.eclipse.emf.edit.provider.AdapterFactoryItemDelegator; -import org.eclipse.emf.edit.provider.ComposedAdapterFactory; -import org.eclipse.emf.edit.provider.IItemPropertyDescriptor; /** * View Provider. */ public class ViewProvider implements IViewProvider { - @Override - public VView provideViewModel(EObject eObject, VViewModelProperties properties) { - final VView view = VViewFactory.eINSTANCE.createView(); - view.setUuid(generateId(eObject.eClass(), null)); - final ComposedAdapterFactory composedAdapterFactory = new ComposedAdapterFactory( - new AdapterFactory[] { - new CustomReflectiveItemProviderAdapterFactory(), - new ComposedAdapterFactory( - ComposedAdapterFactory.Descriptor.Registry.INSTANCE) }); - final AdapterFactoryItemDelegator delegator = new AdapterFactoryItemDelegator( - composedAdapterFactory); - for (final EStructuralFeature feature : getValidFeatures(delegator, eObject)) { - final VControl control; - if (isTableFeature(feature)) { - control = VTableFactory.eINSTANCE.createTableControl(); - final VTableDomainModelReference tableDmr = VTableFactory.eINSTANCE.createTableDomainModelReference(); - tableDmr.setDomainModelReference(createModelReference(feature)); - control.setDomainModelReference(tableDmr); - } else { - control = VViewFactory.eINSTANCE.createControl(); - control.setDomainModelReference(createModelReference(feature)); - } - control.setReadonly(isReadOnly(delegator, eObject, feature)); - control.setUuid(generateId(eObject.eClass(), feature)); - view.getChildren().add(control); - } - composedAdapterFactory.dispose(); - view.setRootEClass(eObject.eClass()); - view.setLoadingProperties(EcoreUtil.copy(properties)); - return view; - } - - private VDomainModelReference createModelReference(final EStructuralFeature feature) { - final VFeaturePathDomainModelReference modelReference = VViewFactory.eINSTANCE - .createFeaturePathDomainModelReference(); - modelReference.setDomainModelEFeature(feature); - return modelReference; - } - - private boolean isTableFeature(EStructuralFeature feature) { - if (feature instanceof EReference) { - final EReference ref = (EReference) feature; - return ref.isMany() && ref.isContainment(); - } - return false; - } - - private boolean isReadOnly(AdapterFactoryItemDelegator delegator, - EObject owner, EStructuralFeature feature) { - if (!feature.isChangeable()) { - return true; - } - final IItemPropertyDescriptor descriptor = delegator.getPropertyDescriptor(owner, - feature); - return !descriptor.canSetProperty(feature); - } + private final ViewCache cache = new ViewCache(); - private boolean isInvalidFeature(EStructuralFeature feature) { - return isContainerReference(feature) || isTransient(feature) || isVolatile(feature); + /** + * Initializes me. + */ + public ViewProvider() { + super(); } - private boolean isContainerReference(EStructuralFeature feature) { - if (feature instanceof EReference) { - final EReference reference = (EReference) feature; - if (reference.isContainer()) { - return true; - } - } - - return false; - } - - private boolean isTransient(EStructuralFeature feature) { - return feature.isTransient(); - } - - private boolean isVolatile(EStructuralFeature feature) { - return feature.isVolatile(); - } - - private Set<EStructuralFeature> getValidFeatures( - AdapterFactoryItemDelegator itemDelegator, EObject eObject) { - final Collection<EStructuralFeature> features = eObject.eClass() - .getEAllStructuralFeatures(); - final Set<EStructuralFeature> featuresToAdd = new LinkedHashSet<EStructuralFeature>(); - IItemPropertyDescriptor propertyDescriptor = null; - for (final EStructuralFeature feature : features) { - propertyDescriptor = itemDelegator - .getPropertyDescriptor(eObject, feature); - if (propertyDescriptor == null || isInvalidFeature(feature)) { - continue; - } - - featuresToAdd.add(feature); - - } - return featuresToAdd; - } - - // TODO this is not unique, because of the use of hashCode, but maybe good enough? - private static String generateId(EClass eClass, EStructuralFeature feature) { - final StringBuilder stringBuilder = new StringBuilder(); - final EPackage ePackage = eClass.getEPackage(); - if (ePackage != null) { - /* might be null with dynamic emf */ - stringBuilder.append(ePackage.getNsURI()); - } - stringBuilder.append("#"); //$NON-NLS-1$ - stringBuilder.append(eClass.getName()); - if (feature != null) { - stringBuilder.append("#"); //$NON-NLS-1$ - stringBuilder.append(feature.getName()); - } - return String.valueOf(stringBuilder.toString().hashCode()); + @Override + public VView provideViewModel(EObject eObject, VViewModelProperties properties) { + final VView result = cache.getView(eObject); + result.setLoadingProperties(EcoreUtil.copy(properties)); + return result; } @Override diff --git a/bundles/org.eclipse.emf.ecp.view.table.celleditor.rcp/src/org/eclipse/emf/ecp/view/internal/table/celleditor/rcp/BooleanCellEditor.java b/bundles/org.eclipse.emf.ecp.view.table.celleditor.rcp/src/org/eclipse/emf/ecp/view/internal/table/celleditor/rcp/BooleanCellEditor.java index 4d2670c9c9..58c979eb29 100644 --- a/bundles/org.eclipse.emf.ecp.view.table.celleditor.rcp/src/org/eclipse/emf/ecp/view/internal/table/celleditor/rcp/BooleanCellEditor.java +++ b/bundles/org.eclipse.emf.ecp.view.table.celleditor.rcp/src/org/eclipse/emf/ecp/view/internal/table/celleditor/rcp/BooleanCellEditor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2011-2015 EclipseSource Muenchen GmbH and others. + * Copyright (c) 2011-2019 EclipseSource Muenchen GmbH and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -10,6 +10,7 @@ * * Contributors: * jfaltermeier - initial API and implementation + * Christian W. Damus - bug 547271 ******************************************************************************/ package org.eclipse.emf.ecp.view.internal.table.celleditor.rcp; @@ -235,8 +236,13 @@ public class BooleanCellEditor extends CellEditor implements ECPCellEditor, ECPC */ @Override public void updateCell(ViewerCell cell, Object value) { + if (!"".equals(cell.getText())) { //$NON-NLS-1$ cell.setText(""); //$NON-NLS-1$ - cell.setImage(getImage(value)); + } + final Image image = getImage(value); + if (cell.getImage() != image) { + cell.setImage(image); + } setCopyTextMarker(cell, value); } diff --git a/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/META-INF/MANIFEST.MF b/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/META-INF/MANIFEST.MF index ac17abcf55..9590679ebb 100644 --- a/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/META-INF/MANIFEST.MF @@ -25,6 +25,7 @@ Require-Bundle: org.eclipse.emf.ecp.edit.swt;bundle-version="[1.21.0,1.22.0)", org.eclipse.emf.ecp.view.core.swt;bundle-version="[1.21.0,1.22.0)";visibility:=reexport, org.eclipse.emf.ecp.ui.view.swt;bundle-version="[1.21.0,1.22.0)" Import-Package: javax.inject;version="1.0.0", + org.eclipse.core.runtime;version="[3.5.0,4.0.0)", org.eclipse.emf.ecp.view.spi.table.model;version="[1.21.0,1.22.0)", org.eclipse.emf.ecp.view.spi.table.swt;version="[1.21.0,1.22.0)", org.eclipse.emfforms.spi.common.report;version="[1.21.0,1.22.0)", diff --git a/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/src/org/eclipse/emf/ecp/view/spi/table/nebula/grid/GridTableViewerComposite.java b/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/src/org/eclipse/emf/ecp/view/spi/table/nebula/grid/GridTableViewerComposite.java index 1f53d734ea..12a9242993 100644 --- a/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/src/org/eclipse/emf/ecp/view/spi/table/nebula/grid/GridTableViewerComposite.java +++ b/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/src/org/eclipse/emf/ecp/view/spi/table/nebula/grid/GridTableViewerComposite.java @@ -11,7 +11,7 @@ * Contributors: * Alexandra Buzila - initial API and implementation * Johannes Faltermeier - initial API and implementation - * Christian W. Damus - bugs 534829, 530314 + * Christian W. Damus - bugs 534829, 530314, 547271 ******************************************************************************/ package org.eclipse.emf.ecp.view.spi.table.nebula.grid; @@ -28,7 +28,9 @@ import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.observable.value.IValueChangeListener; import org.eclipse.core.databinding.observable.value.ValueChangeEvent; import org.eclipse.core.databinding.observable.value.WritableValue; +import org.eclipse.core.runtime.Adapters; import org.eclipse.emf.databinding.EMFDataBindingContext; +import org.eclipse.emf.ecp.edit.spi.swt.table.ECPFilterableCell; import org.eclipse.emf.ecp.view.spi.table.nebula.grid.GridControlSWTRenderer.CustomGridTableViewer; import org.eclipse.emf.ecp.view.spi.table.nebula.grid.menu.GridColumnAction; import org.eclipse.emf.ecp.view.spi.table.nebula.grid.messages.Messages; @@ -535,19 +537,14 @@ public class GridTableViewerComposite extends AbstractTableViewerComposite<GridT return true; } - grid.setRedraw(false); - final GridItem dummyItem = new GridItem(grid, SWT.NONE); + GridItem dummyItem = null; + GridViewerRow viewerRow = null; try { - - dummyItem.setData(element); - final GridViewerRow viewerRow = (GridViewerRow) ((CustomGridTableViewer) tableViewer) - .getViewerRowFromItem(dummyItem); - for (final Widget widget : getColumns()) { - final ColumnConfiguration config = getColumnConfiguration(widget); + // Do we even have a filter to apply? final Object filter = config.matchFilter().getValue(); if (filter == null || String.valueOf(filter).isEmpty()) { continue; @@ -555,20 +552,41 @@ public class GridTableViewerComposite extends AbstractTableViewerComposite<GridT final GridColumn column = (GridColumn) widget; final int columnIndex = tableViewer.getGrid().indexOf(column); - - final ViewerCell cell = viewerRow.getCell(columnIndex); final CellLabelProvider labelProvider = tableViewer.getLabelProvider(columnIndex); - labelProvider.update(cell); - if (!matchesColumnFilter(cell.getText(), filter)) { - return false; - } + // Optimize for the standard case + final ECPFilterableCell filterable = Adapters.adapt(labelProvider, ECPFilterableCell.class); + if (filterable != null) { + // Just get the text and filter it + final String text = filterable.getFilterableText(element); + if (!matchesColumnFilter(text, filter)) { + return false; + } + } else { + // We have a filter, but we need something to filter on + if (dummyItem == null) { + grid.setRedraw(false); + dummyItem = new GridItem(grid, SWT.NONE); + + dummyItem.setData(element); + viewerRow = (GridViewerRow) ((CustomGridTableViewer) tableViewer) + .getViewerRowFromItem(dummyItem); + } - } + // Update the cell, the slow way + final ViewerCell cell = viewerRow.getCell(columnIndex); + labelProvider.update(cell); + if (!matchesColumnFilter(cell.getText(), filter)) { + return false; + } + } + } } finally { - dummyItem.dispose(); - grid.setRedraw(true); + if (dummyItem != null) { + dummyItem.dispose(); + grid.setRedraw(true); + } } return true; diff --git a/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/src/org/eclipse/emf/ecp/view/spi/table/nebula/grid/GridViewerColumnBuilder.java b/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/src/org/eclipse/emf/ecp/view/spi/table/nebula/grid/GridViewerColumnBuilder.java index 508091351a..3ec2d945f9 100644 --- a/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/src/org/eclipse/emf/ecp/view/spi/table/nebula/grid/GridViewerColumnBuilder.java +++ b/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/src/org/eclipse/emf/ecp/view/spi/table/nebula/grid/GridViewerColumnBuilder.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2011-2016 EclipseSource Muenchen GmbH and others. + * Copyright (c) 2011-2019 EclipseSource Muenchen GmbH and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -10,6 +10,7 @@ * * Contributors: * jonas - initial API and implementation + * Christian W. Damus - bug 547271 ******************************************************************************/ package org.eclipse.emf.ecp.view.spi.table.nebula.grid; @@ -18,6 +19,7 @@ import org.eclipse.emfforms.common.Property; import org.eclipse.emfforms.common.Property.ChangeListener; import org.eclipse.emfforms.spi.swt.table.AbstractTableViewerColumnBuilder; import org.eclipse.emfforms.spi.swt.table.ColumnConfiguration; +import org.eclipse.emfforms.spi.swt.table.ViewerRefreshManager; import org.eclipse.jface.databinding.swt.WidgetValueProperty; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; @@ -50,6 +52,12 @@ import org.eclipse.swt.widgets.Widget; public class GridViewerColumnBuilder extends AbstractTableViewerColumnBuilder<GridTableViewer, GridViewerColumn> { /** + * To avoid redundant refreshes from the filtering control listener of every column + * in the grid, refresh requests are posted on this async runnable manager. + */ + private ViewerRefreshManager refreshManager; + + /** * The constructor. * * @param config the {@link ColumnConfiguration} @@ -60,6 +68,8 @@ public class GridViewerColumnBuilder extends AbstractTableViewerColumnBuilder<Gr @Override public GridViewerColumn createViewerColumn(GridTableViewer tableViewer) { + refreshManager = ViewerRefreshManager.getInstance(tableViewer); + return new GridViewerColumn(tableViewer, getConfig().getStyleBits()); } @@ -178,7 +188,7 @@ public class GridViewerColumnBuilder extends AbstractTableViewerColumnBuilder<Gr getConfig().matchFilter().addChangeListener(new ChangeListener<Object>() { @Override public void valueChanged(Property<Object> property, Object oldValue, Object newValue) { - tableViewer.refresh(); + refreshManager.postRefresh(); } }); diff --git a/bundles/org.eclipse.emf.ecp.view.table.ui.swt/src/org/eclipse/emf/ecp/view/spi/table/swt/TableControlSWTRenderer.java b/bundles/org.eclipse.emf.ecp.view.table.ui.swt/src/org/eclipse/emf/ecp/view/spi/table/swt/TableControlSWTRenderer.java index 6a1f27ec81..754b75733b 100644 --- a/bundles/org.eclipse.emf.ecp.view.table.ui.swt/src/org/eclipse/emf/ecp/view/spi/table/swt/TableControlSWTRenderer.java +++ b/bundles/org.eclipse.emf.ecp.view.table.ui.swt/src/org/eclipse/emf/ecp/view/spi/table/swt/TableControlSWTRenderer.java @@ -11,10 +11,12 @@ * Contributors: * Eugen Neufeld - initial API and implementation * Johannes Faltermeier - refactorings - * Christian W. Damus - bugs 544116, 544537, 545686, 530314 + * Christian W. Damus - bugs 544116, 544537, 545686, 530314, 547271 ******************************************************************************/ package org.eclipse.emf.ecp.view.spi.table.swt; +import static org.eclipse.emfforms.spi.swt.table.ViewerRefreshManager.getRefreshRunnable; + import java.net.MalformedURLException; import java.net.URL; import java.text.MessageFormat; @@ -43,7 +45,9 @@ import org.eclipse.core.databinding.observable.map.IObservableMap; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.property.value.IValueProperty; import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Platform; import org.eclipse.emf.common.command.Command; import org.eclipse.emf.common.command.CompoundCommand; import org.eclipse.emf.common.notify.Notification; @@ -63,6 +67,7 @@ import org.eclipse.emf.ecp.edit.spi.swt.table.ECPCellEditor; import org.eclipse.emf.ecp.edit.spi.swt.table.ECPCellEditorComparator; import org.eclipse.emf.ecp.edit.spi.swt.table.ECPCustomUpdateCellEditor; import org.eclipse.emf.ecp.edit.spi.swt.table.ECPElementAwareCellEditor; +import org.eclipse.emf.ecp.edit.spi.swt.table.ECPFilterableCell; import org.eclipse.emf.ecp.edit.spi.swt.table.ECPViewerAwareCellEditor; import org.eclipse.emf.ecp.edit.spi.swt.util.ECPDialogExecutor; import org.eclipse.emf.ecp.view.internal.table.swt.Activator; @@ -149,6 +154,7 @@ import org.eclipse.emfforms.spi.swt.table.TableViewerCreator; import org.eclipse.emfforms.spi.swt.table.TableViewerFactory; import org.eclipse.emfforms.spi.swt.table.TableViewerSWTBuilder; import org.eclipse.emfforms.spi.swt.table.TableViewerSWTCustomization; +import org.eclipse.emfforms.spi.swt.table.ViewerRefreshManager; import org.eclipse.emfforms.spi.swt.table.action.ActionBar; import org.eclipse.emfforms.spi.swt.table.action.ActionConfiguration; import org.eclipse.emfforms.spi.swt.table.action.ActionConfigurationBuilder; @@ -416,6 +422,8 @@ public class TableControlSWTRenderer extends AbstractControlSWTRenderer<VTableCo tableViewerSWTBuilder .configureTable(TableConfigurationBuilder.from(tableViewerSWTBuilder) .dataMapEntry(TableConfiguration.DMR, dmrToCheck) + .dataMapEntry(ViewerRefreshManager.REFRESH_MANAGER, + (ViewerRefreshManager) this::postRefresh) .build()); regularColumnsStartIndex = 0; @@ -1511,6 +1519,18 @@ public class TableControlSWTRenderer extends AbstractControlSWTRenderer<VTableCo } /** + * Post a refresh request on the asynchronous refresh manager. + * + * @since 1.21 + */ + protected void postRefresh() { + final Viewer viewer = getTableViewer(); + if (viewer != null && !viewer.getControl().isDisposed()) { + getRunnableManager().executeAsync(getRefreshRunnable(viewer)); + } + } + + /** * Returns the add button created by the framework. * * @deprecated use {@link #getControlForAction(String)} instead @@ -1822,7 +1842,6 @@ public class TableControlSWTRenderer extends AbstractControlSWTRenderer<VTableCo */ private final class TableControlDropAdapter extends EditingDomainViewerDropAdapter { - private final AbstractTableViewer tableViewer; private EObject eObject; private EStructuralFeature eStructuralFeature; private List<Object> list; @@ -1830,7 +1849,6 @@ public class TableControlSWTRenderer extends AbstractControlSWTRenderer<VTableCo @SuppressWarnings("unchecked") TableControlDropAdapter(EditingDomain domain, Viewer viewer, AbstractTableViewer tableViewer) { super(domain, viewer); - this.tableViewer = tableViewer; try { final Setting setting = getEMFFormsDatabinding().getSetting(getDMRToMultiReference(), getViewModelContext().getDomainModel()); @@ -1907,7 +1925,7 @@ public class TableControlSWTRenderer extends AbstractControlSWTRenderer<VTableCo } domain.getCommandStack().execute(command); - tableViewer.refresh(); + postRefresh(); } } @@ -2178,7 +2196,8 @@ public class TableControlSWTRenderer extends AbstractControlSWTRenderer<VTableCo // Update these specific objects getTableViewer().update(updates.toArray(), null); } else { - // Just refresh everything + // Just refresh everything. We are already in the RunnableManager + // context, so don't post but do it directly getTableViewer().refresh(); } } @@ -2477,7 +2496,7 @@ public class TableControlSWTRenderer extends AbstractControlSWTRenderer<VTableCo private void sortAndReveal(Object toReveal) { Display.getDefault().asyncExec(() -> { - getTableViewer().refresh(); + postRefresh(); getTableViewer().reveal(toReveal); }); } @@ -2490,7 +2509,7 @@ public class TableControlSWTRenderer extends AbstractControlSWTRenderer<VTableCo * @author emueller * */ - public class ECPCellLabelProvider extends ObservableMapCellLabelProvider implements IColorProvider { + public class ECPCellLabelProvider extends ObservableMapCellLabelProvider implements IColorProvider, IAdaptable { private final EStructuralFeature feature; private final CellEditor cellEditor; @@ -2555,21 +2574,50 @@ public class TableControlSWTRenderer extends AbstractControlSWTRenderer<VTableCo @Override public void update(ViewerCell cell) { final EObject element = (EObject) cell.getElement(); - final Object value = attributeMaps[0].get(element); + final Object value = getValue(element); if (ECPCustomUpdateCellEditor.class.isInstance(cellEditor)) { ((ECPCustomUpdateCellEditor) cellEditor).updateCell(cell, value); - } else if (ECPCellEditor.class.isInstance(cellEditor)) { - final ECPCellEditor ecpCellEditor = (ECPCellEditor) cellEditor; - final String text = ecpCellEditor.getFormatedString(value); - cell.setText(text == null ? "" : text); //$NON-NLS-1$ - cell.setImage(ecpCellEditor.getImage(value)); } else { - cell.setText(value == null ? "" : value.toString()); //$NON-NLS-1$ - cell.getControl().setData(CUSTOM_VARIANT, "org_eclipse_emf_ecp_edit_cellEditor_string"); //$NON-NLS-1$ + String text; + Image image = null; + + if (ECPCellEditor.class.isInstance(cellEditor)) { + final ECPCellEditor ecpCellEditor = (ECPCellEditor) cellEditor; + text = Objects.toString(ecpCellEditor.getFormatedString(value), ""); //$NON-NLS-1$ + image = ecpCellEditor.getImage(value); + } else { + text = Objects.toString(value, ""); //$NON-NLS-1$ + cell.getControl().setData(CUSTOM_VARIANT, "org_eclipse_emf_ecp_edit_cellEditor_string"); //$NON-NLS-1$ + } + + if (!Objects.equals(text, cell.getText())) { + cell.setText(text); + } + + // Don't try to compare images + if (image != null || cell.getImage() != null) { + cell.setImage(image); + } } - cell.setForeground(getForeground(element)); - cell.setBackground(getBackground(element)); + final Color foreground = getForeground(element); + if (!Objects.equals(cell.getForeground(), foreground)) { + cell.setForeground(foreground); + } + final Color background = getBackground(element); + if (!Objects.equals(cell.getBackground(), background)) { + cell.setBackground(background); + } + } + + /** + * Get the value for an {@code object} from my observable map. + * + * @param object an object to look up the value for + * @return its value + */ + Object getValue(Object object) { + return attributeMaps[0].get(object); } /** @@ -2619,6 +2667,30 @@ public class TableControlSWTRenderer extends AbstractControlSWTRenderer<VTableCo protected VDomainModelReference getDmr() { return dmr; } + + @Override + public <T> T getAdapter(Class<T> adapter) { + T result = null; + + // For custom cell update, we must ask the cell editor to render a string for filtering + if (adapter == ECPFilterableCell.class && !(cellEditor instanceof ECPCustomUpdateCellEditor)) { + ECPFilterableCell filterable = null; + + if (cellEditor instanceof ECPCellEditor) { + final ECPCellEditor ecpCellEditor = (ECPCellEditor) cellEditor; + filterable = object -> ecpCellEditor.getFormatedString(getValue(object)); + } else { + filterable = object -> Objects.toString(object, ""); //$NON-NLS-1$ + } + + result = adapter.cast(filterable); + } else { + result = Platform.getAdapterManager().getAdapter(this, adapter); + } + + return result; + } + } /** diff --git a/bundles/org.eclipse.emfforms.swt.table/src/org/eclipse/emfforms/spi/swt/table/AbstractTableViewerComposite.java b/bundles/org.eclipse.emfforms.swt.table/src/org/eclipse/emfforms/spi/swt/table/AbstractTableViewerComposite.java index bfc69730cc..9cfef4836c 100644 --- a/bundles/org.eclipse.emfforms.swt.table/src/org/eclipse/emfforms/spi/swt/table/AbstractTableViewerComposite.java +++ b/bundles/org.eclipse.emfforms.swt.table/src/org/eclipse/emfforms/spi/swt/table/AbstractTableViewerComposite.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2011-2016 EclipseSource Muenchen GmbH and others. + * Copyright (c) 2011-2019 EclipseSource Muenchen GmbH and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -10,6 +10,7 @@ * * Contributors: * jonas - initial API and implementation + * Christian W. Damus - bug 547271 ******************************************************************************/ package org.eclipse.emfforms.spi.swt.table; @@ -165,6 +166,12 @@ public abstract class AbstractTableViewerComposite<V extends AbstractTableViewer final V tableViewer = createTableViewer(customization, viewerComposite); + final TableConfiguration configuration = customization.getTableConfiguration(); + if (configuration != null) { + // Pump in the configuration data + configuration.getData().forEach(tableViewer::setData); + } + // If an action configuration was configured, bind key bindings to the viewer final Optional<ActionConfiguration> actionConfiguration = customization.getActionConfiguration(); if (actionConfiguration.isPresent()) { diff --git a/bundles/org.eclipse.emfforms.swt.table/src/org/eclipse/emfforms/spi/swt/table/ViewerRefreshManager.java b/bundles/org.eclipse.emfforms.swt.table/src/org/eclipse/emfforms/spi/swt/table/ViewerRefreshManager.java new file mode 100644 index 0000000000..5dd4d19d7b --- /dev/null +++ b/bundles/org.eclipse.emfforms.swt.table/src/org/eclipse/emfforms/spi/swt/table/ViewerRefreshManager.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2019 Christian W. Damus and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christian W. Damus - initial API and implementation + ******************************************************************************/ +package org.eclipse.emfforms.spi.swt.table; + +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.widgets.Control; + +/** + * Protocol for asynchronous non-redundant refresh of a viewer. + * This is {@linkplain Viewer#setData(String, Object) associated with the table viewer} + * under the {@link #REFRESH_MANAGER} key. + * + * @since 1.21 + */ +@FunctionalInterface +public interface ViewerRefreshManager { + + /** Viewer data key for the refresh manager. */ + String REFRESH_MANAGER = "refreshManager"; //$NON-NLS-1$ + + /** + * Post an asynchronous request to refresh the table viewer. + */ + void postRefresh(); + + /** + * Obtain the refresh manager instance for the given {@code viewer}. + * + * @param viewer a viewer + * @return its refresh manager or a simple default implementaiton; never {@code null} + */ + static ViewerRefreshManager getInstance(Viewer viewer) { + final Object result = viewer.getData(REFRESH_MANAGER); + + if (result instanceof ViewerRefreshManager) { + return (ViewerRefreshManager) result; + } + + final Runnable refresher = getRefreshRunnable(viewer); + return () -> viewer.getControl().getDisplay().asyncExec(refresher); + } + + /** + * Obtain a runnable that {@linkplain Viewer#refresh() refreshes} a {@code viewer}. + * + * @param viewer a viewer to refresh + * @return the refresh manager + * + * @see Viewer#refresh() + */ + static Runnable getRefreshRunnable(Viewer viewer) { + final Control control = viewer.getControl(); + + return () -> { + if (!control.isDisposed()) { + viewer.refresh(); + } + }; + } + +} diff --git a/tests/org.eclipse.emf.ecp.view.model.provider.generator.test/src/org/eclipse/emf/ecp/view/model/generator/ViewProvider_Test.java b/tests/org.eclipse.emf.ecp.view.model.provider.generator.test/src/org/eclipse/emf/ecp/view/model/generator/ViewProvider_Test.java index ab7c031aca..8fb9f2de3c 100644 --- a/tests/org.eclipse.emf.ecp.view.model.provider.generator.test/src/org/eclipse/emf/ecp/view/model/generator/ViewProvider_Test.java +++ b/tests/org.eclipse.emf.ecp.view.model.provider.generator.test/src/org/eclipse/emf/ecp/view/model/generator/ViewProvider_Test.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2011-2018 EclipseSource Muenchen GmbH and others. + * Copyright (c) 2011-2019 EclipseSource Muenchen GmbH and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -10,32 +10,66 @@ * * Contributors: * Lucas Koehler - initial API and implementation + * Christian W. Damus - bug 547271 ******************************************************************************/ package org.eclipse.emf.ecp.view.model.generator; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.eclipse.emf.common.command.BasicCommandStack; +import org.eclipse.emf.common.notify.AdapterFactory; +import org.eclipse.emf.common.util.ECollections; +import org.eclipse.emf.common.util.EList; +import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EAttribute; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EPackage; import org.eclipse.emf.ecore.EReference; +import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.EcoreFactory; import org.eclipse.emf.ecore.EcorePackage; +import org.eclipse.emf.ecore.InternalEObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.resource.impl.ResourceImpl; +import org.eclipse.emf.ecore.util.BasicExtendedMetaData.EClassifierExtendedMetaData; +import org.eclipse.emf.ecore.util.EObjectContainmentWithInverseEList; +import org.eclipse.emf.ecore.util.EObjectEList; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.emf.ecp.view.spi.model.VControl; import org.eclipse.emf.ecp.view.spi.model.VDomainModelReference; +import org.eclipse.emf.ecp.view.spi.model.VElement; import org.eclipse.emf.ecp.view.spi.model.VFeaturePathDomainModelReference; import org.eclipse.emf.ecp.view.spi.model.VView; import org.eclipse.emf.ecp.view.spi.model.VViewFactory; import org.eclipse.emf.ecp.view.spi.model.VViewModelLoadingProperties; import org.eclipse.emf.ecp.view.spi.table.model.VTableControl; import org.eclipse.emf.ecp.view.spi.table.model.VTableDomainModelReference; +import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain; +import org.eclipse.emf.edit.domain.EditingDomain; import org.junit.Before; import org.junit.Test; @@ -168,10 +202,92 @@ public class ViewProvider_Test { assertSame(multiContainmentRef, featureDmr.getDomainModelEFeature()); } + /** + * Verify that controls' generated UUIDs actually are unique within the view (there is no + * guarantee that they are unique globally, but the {@link ViewProvider} does not need that). + */ + @Test + public void controlsHaveUniqueUUIDs() { + final EClass eClass = mock(EClass.class, + withSettings().extraInterfaces(InternalEObject.class, EClassifierExtendedMetaData.Holder.class)); + when(eClass.eContainer()).thenReturn(this.eClass.eContainer()); + when(eClass.getEPackage()).thenReturn(this.eClass.getEPackage()); + when(eClass.getName()).thenReturn("Mock"); + when(eClass.getESuperTypes()).thenReturn(ECollections.emptyEList()); + when(((EClassifierExtendedMetaData.Holder) eClass).getExtendedMetaData()) + .thenReturn(mock(EClassifierExtendedMetaData.class)); + + // Repeat the exact same feature multiple times to create controls + final EList<EStructuralFeature> features = new EObjectContainmentWithInverseEList<>(EStructuralFeature.class, + (InternalEObject) eClass, EcorePackage.ECLASS__ESTRUCTURAL_FEATURES, + EcorePackage.ESTRUCTURAL_FEATURE__ECONTAINING_CLASS); + final EReference ref = EcoreFactory.eINSTANCE.createEReference(); + ref.setEType(refType); + ref.setName("ref"); + final EList<EReference> references = new EObjectEList<>(EReference.class, (InternalEObject) eClass, + EcorePackage.ECLASS__EREFERENCES); + references.addAll(Collections.nCopies(500, ref)); + features.addAll(references); + when(eClass.getEStructuralFeatures()).thenReturn(features); + when(eClass.getEAllStructuralFeatures()).thenReturn(ECollections.unmodifiableEList(features)); + when(eClass.getEReferences()).thenReturn(references); + when(eClass.getEAllReferences()).thenReturn(references); + when(eClass.getEAttributes()).thenReturn(ECollections.emptyEList()); + when(eClass.getEAllAttributes()).thenReturn(ECollections.emptyEList()); + final EObject object = EcoreUtil.create(eClass); + + final VView view = viewProvider.provideViewModel(object, viewProperties); + + final Set<String> uuids = new HashSet<>(); + controls(view).forEach(control -> { + final String uuid = control.getUuid(); + assertThat("No UUID generated", uuid, notNullValue()); + assertThat("Non-unique UUID: " + uuid, uuids.add(uuid)); + }); + + assertThat("Didn't get a control for each occurrence of the feature", uuids.size(), + is(eClass.getEAllStructuralFeatures().size())); + } + + /** + * Verify that the item provider is used to determine read-only state of controls. + */ + @Test + public void readOnlyControls() { + final EReference ref = EcoreFactory.eINSTANCE.createEReference(); + ref.setEType(refType); + ref.setName("ref"); + eClass.getEStructuralFeatures().add(ref); + + final Map<Resource, Boolean> readOnly = new HashMap<>(); + final EditingDomain domain = new AdapterFactoryEditingDomain(mock(AdapterFactory.class), + new BasicCommandStack(), readOnly); + final ResourceSet rset = domain.getResourceSet(); + final Resource resource = new ResourceImpl(URI.createURI("test:/resource")); + final EObject object = EcoreUtil.create(eClass); + resource.getContents().add(object); + rset.getResources().add(resource); + readOnly.put(resource, true); + + final VView view = viewProvider.provideViewModel(object, viewProperties); + final int[] count = { 0 }; + controls(view) + .peek(__ -> count[0]++) + .forEach(control -> assertThat("Control not read-only", control.isEffectivelyReadonly(), is(true))); + assertThat("No controls found", count[0], not(0)); + } + private void assertView(final VView view) { assertNotNull(view); assertNotNull(view.getUuid()); assertFalse(view.getUuid().isEmpty()); assertEquals(1, view.getChildren().size()); } + + private Stream<VControl> controls(VElement viewModel) { + return StreamSupport + .stream(Spliterators.spliteratorUnknownSize(EcoreUtil.getAllContents(Collections.singleton(viewModel)), + Spliterator.DISTINCT | Spliterator.NONNULL | Spliterator.ORDERED), false) + .filter(VControl.class::isInstance).map(VControl.class::cast); + } } diff --git a/tests/org.eclipse.emf.ecp.view.table.ui.nebula.grid.test/src/org/eclipse/emf/ecp/view/internal/table/nebula/grid/GridControlRenderer_PTest.java b/tests/org.eclipse.emf.ecp.view.table.ui.nebula.grid.test/src/org/eclipse/emf/ecp/view/internal/table/nebula/grid/GridControlRenderer_PTest.java index d65effda01..0f8ee38a4a 100644 --- a/tests/org.eclipse.emf.ecp.view.table.ui.nebula.grid.test/src/org/eclipse/emf/ecp/view/internal/table/nebula/grid/GridControlRenderer_PTest.java +++ b/tests/org.eclipse.emf.ecp.view.table.ui.nebula.grid.test/src/org/eclipse/emf/ecp/view/internal/table/nebula/grid/GridControlRenderer_PTest.java @@ -74,6 +74,7 @@ import org.eclipse.emf.ecp.view.template.model.VTViewTemplateProvider; import org.eclipse.emf.ecp.view.template.style.keybinding.model.VTKeyBinding; import org.eclipse.emf.ecp.view.template.style.keybinding.model.VTKeyBindings; import org.eclipse.emf.ecp.view.template.style.keybinding.model.VTKeybindingFactory; +import org.eclipse.emf.ecp.view.test.common.swt.spi.SWTTestUtil; import org.eclipse.emf.edit.domain.EditingDomain; import org.eclipse.emfforms.common.Optional; import org.eclipse.emfforms.spi.common.converter.EStructuralFeatureValueConverterService; @@ -522,7 +523,7 @@ public class GridControlRenderer_PTest extends AbstractControl_PTest<VTableContr columnConfiguration.getEnabledFeatures() .contains(ColumnConfiguration.FEATURE_COLUMN_FILTER)); - assertEquals(expectedRows, grid.getItems().length); // 3 players/rows defined in mockSampleDataSet() + assertEquals(expectedRows, countItems(grid)); // 3 players/rows defined in mockSampleDataSet() /* * test filtering @@ -537,34 +538,34 @@ public class GridControlRenderer_PTest extends AbstractControl_PTest<VTableContr columnConfiguration.matchFilter().setValue("er"); switch (dataset) { case Simple: - assertEquals(3, filterVisible(grid.getItems()).length); + assertEquals(3, countVisible(grid)); break; default: // complex case, has only two logins (a bot doesn't have one) - assertEquals(2, filterVisible(grid.getItems()).length); + assertEquals(2, countVisible(grid)); } columnConfiguration.matchFilter().setValue("foo"); - assertEquals(0, filterVisible(grid.getItems()).length); + assertEquals(0, countVisible(grid)); // double check, that extending a filter which lead to an empty result still created an empty result columnConfiguration.matchFilter().setValue("foo2"); - assertEquals(0, filterVisible(grid.getItems()).length); + assertEquals(0, countVisible(grid)); columnConfiguration.matchFilter().resetToDefault(); - assertEquals(expectedRows, filterVisible(grid.getItems()).length); + assertEquals(expectedRows, countVisible(grid)); columnConfiguration.matchFilter().setValue("Kliver"); - assertEquals(1, filterVisible(grid.getItems()).length); + assertEquals(1, countVisible(grid)); columnConfiguration.matchFilter().resetToDefault(); columnConfiguration.matchFilter().setValue("branzfeckenbauer"); switch (dataset) { case Simple: - assertEquals(0, filterVisible(grid.getItems()).length); + assertEquals(0, countVisible(grid)); break; default: // complex case, there is exactly one matching login - assertEquals(1, filterVisible(grid.getItems()).length); + assertEquals(1, countVisible(grid)); } /* @@ -576,8 +577,8 @@ public class GridControlRenderer_PTest extends AbstractControl_PTest<VTableContr is(false)); assertFilterMenuChecked(tableViewerComposite, null); - assertEquals(3, grid.getItems().length); - assertEquals(3, filterVisible(grid.getItems()).length); + assertEquals(3, countItems(grid)); + assertEquals(3, countVisible(grid)); } /** @@ -593,7 +594,7 @@ public class GridControlRenderer_PTest extends AbstractControl_PTest<VTableContr final Event event = new Event(); // Some menu actions are not defined if the mouse is not over a grid cell - final Rectangle cell = composite.getTableViewer().getGrid().getItem(0).getBounds(1); + final Rectangle cell = allItems(composite.getTableViewer().getGrid())[0].getBounds(1); event.type = SWT.MouseMove; event.x = cell.x + cell.width / 2; event.y = cell.y + cell.height / 2; @@ -651,12 +652,12 @@ public class GridControlRenderer_PTest extends AbstractControl_PTest<VTableContr is(true)); columnConfiguration.matchFilter().setValue("foo"); - assertEquals(0, filterVisible(grid.getItems()).length); + assertEquals(0, countVisible(grid)); tableViewerComposite.setFilteringMode(null); columnConfiguration.visible().setValue(Boolean.FALSE); - assertEquals(expectedRows, filterVisible(grid.getItems()).length); + assertEquals(expectedRows, countVisible(grid)); /* * test for Gerrit #110529 (filter again after filters have been hidden) @@ -668,7 +669,7 @@ public class GridControlRenderer_PTest extends AbstractControl_PTest<VTableContr is(true)); columnConfiguration.matchFilter().setValue("bar"); - assertEquals(0, filterVisible(grid.getItems()).length); + assertEquals(0, countVisible(grid)); columnConfiguration.matchFilter().resetToDefault(); tableViewerComposite.setFilteringMode(null); @@ -677,7 +678,7 @@ public class GridControlRenderer_PTest extends AbstractControl_PTest<VTableContr // will result in a NPE without Gerrit #110529 columnConfiguration.visible().setValue(Boolean.FALSE); - assertEquals(expectedRows, filterVisible(grid.getItems()).length); + assertEquals(expectedRows, countVisible(grid)); } @@ -701,8 +702,8 @@ public class GridControlRenderer_PTest extends AbstractControl_PTest<VTableContr assertThat("Feature not enabled/supported. Check ColumnConfiguration.FEATURES?", columnConfiguration.getEnabledFeatures(), hasItem(ColumnConfiguration.FEATURE_COLUMN_REGEX_FILTER)); - assertThat("Wrong number of rrows filtered", - grid.getItems().length, is(expectedRows)); // 3 players/rows defined in mockSampleDataSet() + assertThat("Wrong number of rows filtered", + countItems(grid), is(expectedRows)); // 3 players/rows defined in mockSampleDataSet() /* * test filtering @@ -714,63 +715,63 @@ public class GridControlRenderer_PTest extends AbstractControl_PTest<VTableContr columnConfiguration.matchFilter().setValue("er"); switch (dataset) { case Simple: - assertThat("No rows should be filtered", filterVisible(grid.getItems()).length, is(3)); + assertThat("No rows should be filtered", countVisible(grid), is(3)); break; default: // complex case, has only two logins (a bot doesn't have one) - assertThat("One row should be filtered", filterVisible(grid.getItems()).length, is(2)); + assertThat("One row should be filtered", countVisible(grid), is(2)); } // Match only 'er' at the end of the string columnConfiguration.matchFilter().setValue("er$"); switch (dataset) { case Simple: - assertThat("One row should be filtered", filterVisible(grid.getItems()).length, is(2)); + assertThat("One row should be filtered", countVisible(grid), is(2)); break; default: // complex case, has only two logins (a bot doesn't have one) - assertThat("Two rows should be filtered", filterVisible(grid.getItems()).length, is(1)); + assertThat("Two rows should be filtered", countVisible(grid), is(1)); } columnConfiguration.matchFilter().setValue("foo"); - assertThat("All rows should be filtered", filterVisible(grid.getItems()).length, is(0)); + assertThat("All rows should be filtered", countVisible(grid), is(0)); // double check, that extending a filter which lead to an empty result still created an empty result columnConfiguration.matchFilter().setValue("foo2"); - assertThat("All rows should be filtered", filterVisible(grid.getItems()).length, is(0)); + assertThat("All rows should be filtered", countVisible(grid), is(0)); // but an invalid regex doesn't filter anything (user should see examples in the grid to // be guides in formulating the regex) columnConfiguration.matchFilter().setValue("([a].*\\1"); - assertThat("No rows should be filtered", filterVisible(grid.getItems()).length, is(expectedRows)); + assertThat("No rows should be filtered", countVisible(grid), is(expectedRows)); columnConfiguration.matchFilter().setValue("([a]).*\\1"); switch (dataset) { case Simple: - assertThat("Two rows should be filtered", filterVisible(grid.getItems()).length, is(1)); + assertThat("Two rows should be filtered", countVisible(grid), is(1)); break; default: // complex case, where the bot hasn't a login - assertThat("Two rows should be filtered", filterVisible(grid.getItems()).length, is(1)); + assertThat("Two rows should be filtered", countVisible(grid), is(1)); } columnConfiguration.matchFilter().setValue("([ae]).*\\1"); switch (dataset) { case Simple: - assertThat("One row should be filtered", filterVisible(grid.getItems()).length, is(2)); + assertThat("One row should be filtered", countVisible(grid), is(2)); break; default: // complex case, where the bot hasn't a login - assertThat("Two rows should be filtered", filterVisible(grid.getItems()).length, is(1)); + assertThat("Two rows should be filtered", countVisible(grid), is(1)); } columnConfiguration.matchFilter().resetToDefault(); - assertThat("No rows should be filtered", filterVisible(grid.getItems()).length, is(expectedRows)); + assertThat("No rows should be filtered", countVisible(grid), is(expectedRows)); columnConfiguration.matchFilter().setValue("branzfeckenbauer"); switch (dataset) { case Simple: - assertThat("All rows should be filtered", filterVisible(grid.getItems()).length, is(0)); + assertThat("All rows should be filtered", countVisible(grid), is(0)); break; default: // complex case, there is exactly one matching login - assertThat("Two rows should be filtered", filterVisible(grid.getItems()).length, is(1)); + assertThat("Two rows should be filtered", countVisible(grid), is(1)); } /* @@ -782,13 +783,13 @@ public class GridControlRenderer_PTest extends AbstractControl_PTest<VTableContr assertThat("Filter control still showing", columnConfiguration.showFilterControl().getValue(), is(false)); - assertThat("Wrong number of items in the grid", grid.getItems().length, is(3)); - assertThat("No rows should be filtered", filterVisible(grid.getItems()).length, is(3)); + assertThat("Wrong number of items in the grid", countItems(grid), is(3)); + assertThat("No rows should be filtered", countVisible(grid), is(3)); } - private GridItem[] filterVisible(GridItem[] items) { + private GridItem[] filterVisible(Grid grid) { final List<GridItem> visibleItems = new ArrayList<GridItem>(); - for (final GridItem item : items) { + for (final GridItem item : allItems(grid)) { if (!item.isVisible()) { continue; } @@ -797,6 +798,19 @@ public class GridControlRenderer_PTest extends AbstractControl_PTest<VTableContr return visibleItems.toArray(new GridItem[] {}); } + private GridItem[] allItems(Grid grid) { + SWTTestUtil.waitForUIThread(); // The table refresh is asynchronous + return grid.getItems(); + } + + private int countItems(Grid grid) { + return allItems(grid).length; + } + + private int countVisible(Grid grid) { + return filterVisible(grid).length; + } + @Override public void renderValidationIconLabelAlignmentLeft() throws NoRendererFoundException, NoPropertyDescriptorFoundExeption { @@ -849,6 +863,7 @@ public class GridControlRenderer_PTest extends AbstractControl_PTest<VTableContr assertFalse(duplicateRowButton.isPresent()); } + @Test public void testActionKeyBindings() throws DatabindingFailedException, NoRendererFoundException, NoPropertyDescriptorFoundExeption { |