1 /*******************************************************************************
2  * Copyright (c) 2010-2014 BestSolution.at and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  * Tom Schindl <tom.schindl@bestsolution.at> - initial API and implementation
13  * Marco Descher <marco@descher.at> - Bug 422465
14  * Steven Spungin <steven@spungin.tv> - Bug 437951, Bug 439709
15  * Olivier Prouvost <olivier.prouvost@opcoach.com> Bug 403583, 472658
16  ******************************************************************************/
17 package org.eclipse.e4.tools.emf.ui.common.component;
18 
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.net.URL;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.List;
25 import java.util.Objects;
26 
27 import javax.annotation.PreDestroy;
28 import javax.inject.Inject;
29 
30 import org.eclipse.core.databinding.observable.list.IObservableList;
31 import org.eclipse.core.databinding.observable.value.WritableValue;
32 import org.eclipse.core.databinding.property.value.IValueProperty;
33 import org.eclipse.core.resources.IProject;
34 import org.eclipse.core.runtime.FileLocator;
35 import org.eclipse.e4.core.di.annotations.Optional;
36 import org.eclipse.e4.core.services.nls.Translation;
37 import org.eclipse.e4.core.services.translation.TranslationService;
38 import org.eclipse.e4.tools.emf.ui.common.AbstractElementEditorContribution;
39 import org.eclipse.e4.tools.emf.ui.common.Util;
40 import org.eclipse.e4.tools.emf.ui.internal.Messages;
41 import org.eclipse.e4.tools.emf.ui.internal.common.ModelEditor;
42 import org.eclipse.e4.tools.emf.ui.internal.common.component.ControlFactory;
43 import org.eclipse.e4.tools.emf.ui.internal.common.properties.ProjectOSGiTranslationProvider;
44 import org.eclipse.e4.tools.services.IClipboardService.Handler;
45 import org.eclipse.e4.tools.services.IResourcePool;
46 import org.eclipse.e4.tools.services.impl.ResourceBundleTranslationProvider;
47 import org.eclipse.e4.ui.model.application.MApplicationElement;
48 import org.eclipse.e4.ui.model.application.ui.MUIElement;
49 import org.eclipse.e4.ui.model.application.ui.MUILabel;
50 import org.eclipse.emf.databinding.EMFDataBindingContext;
51 import org.eclipse.emf.databinding.FeaturePath;
52 import org.eclipse.emf.databinding.edit.EMFEditProperties;
53 import org.eclipse.emf.ecore.EAttribute;
54 import org.eclipse.emf.ecore.EObject;
55 import org.eclipse.emf.edit.domain.EditingDomain;
56 import org.eclipse.jface.action.Action;
57 import org.eclipse.jface.resource.ImageDescriptor;
58 import org.eclipse.jface.resource.ImageRegistry;
59 import org.eclipse.swt.SWT;
60 import org.eclipse.swt.custom.CTabFolder;
61 import org.eclipse.swt.custom.CTabItem;
62 import org.eclipse.swt.custom.ScrolledComposite;
63 import org.eclipse.swt.events.ControlAdapter;
64 import org.eclipse.swt.events.ControlEvent;
65 import org.eclipse.swt.graphics.Image;
66 import org.eclipse.swt.graphics.Rectangle;
67 import org.eclipse.swt.layout.GridData;
68 import org.eclipse.swt.layout.GridLayout;
69 import org.eclipse.swt.widgets.Composite;
70 import org.eclipse.swt.widgets.Control;
71 
72 /**
73  * @param <M> type of the master object
74  */
75 public abstract class AbstractComponentEditor<M> {
76 	private static final String GREY_SUFFIX = "Grey"; //$NON-NLS-1$
77 
78 	private static final String CSS_CLASS_KEY = "org.eclipse.e4.ui.css.CssClassName"; //$NON-NLS-1$
79 
80 	private static final int MAX_IMG_SIZE = 16;
81 
82 	private final WritableValue<M> master = new WritableValue<>();
83 
84 	public static final int SEARCH_IMAGE = 0;
85 	public static final int TABLE_ADD_IMAGE = 1;
86 	public static final int TABLE_DELETE_IMAGE = 2;
87 	public static final int ARROW_UP = 3;
88 	public static final int ARROW_DOWN = 4;
89 
90 	protected static final int VERTICAL_LIST_WIDGET_INDENT = 10;
91 
92 	private final List<Image> createdImages = new ArrayList<>();
93 
94 	@Inject
95 	private EditingDomain editingDomain;
96 	@Inject
97 	private ModelEditor editor;
98 	@Inject
99 	public IResourcePool resourcePool;
100 
101 	@Inject
102 	@Optional
103 	protected IProject project;
104 
105 	@Inject
106 	@Translation
107 	protected Messages Messages;
108 
109 	@Inject
110 	@Optional
111 	private ProjectOSGiTranslationProvider translationProvider;
112 
113 
114 	private Composite editorControl;
115 
116 	private IdGenerator generator;
117 
getEditingDomain()118 	public EditingDomain getEditingDomain() {
119 		return editingDomain;
120 	}
121 
getEditor()122 	public ModelEditor getEditor() {
123 		return editor;
124 	}
125 
getMaster()126 	public WritableValue<M> getMaster() {
127 		return master;
128 	}
129 
setElementId(Object element)130 	protected void setElementId(Object element) {
131 		if (getEditor().isAutoCreateElementId() && element instanceof MApplicationElement) {
132 			final MApplicationElement el = (MApplicationElement) element;
133 			if (el.getElementId() == null || el.getElementId().trim().length() == 0) {
134 				el.setElementId(Util.getDefaultElementId(((EObject) getMaster().getValue()).eResource(), el,
135 						getEditor().getProject()));
136 			}
137 		}
138 	}
139 
createImage(String key)140 	public Image createImage(String key) {
141 		return resourcePool.getImageUnchecked(key);
142 	}
143 
createImageDescriptor(String key)144 	public ImageDescriptor createImageDescriptor(String key) {
145 		if (key == null) {
146 			return null;
147 		}
148 		return ImageDescriptor.createFromImage(createImage(key));
149 	}
150 
getComponentImages()151 	private ImageRegistry getComponentImages() {
152 		return editor.getComponentImages();
153 	}
154 
155 	/**
156 	 * Get the image described in element if this is a MUILabel
157 	 *
158 	 * @param element the element in tree to be displayed
159 	 * @return image of element if iconUri is not empty (returns bad image if bad
160 	 *         URI), else returns null
161 	 */
getImageFromIconURI(MUILabel element)162 	public Image getImageFromIconURI(MUILabel element) {
163 
164 		Image img = null;
165 		// Returns only an image if there is a non empty Icon URI
166 		final String iconUri = element.getIconURI();
167 		if (iconUri != null && iconUri.trim().length() > 0) {
168 			final boolean greyVersion = shouldBeGrey(element);
169 			// Is this image already loaded ?
170 			img = getImage(iconUri, greyVersion);
171 			if (img == null) {
172 				// No image registered yet in ImageRegistry...
173 				final ImageDescriptor desc = getImageDescriptorFromUri(iconUri);
174 
175 				// Can now add this image in the image registry
176 				getComponentImages().put(iconUri, desc);
177 				img = getImage(iconUri, greyVersion);
178 			}
179 		}
180 
181 		return img;
182 	}
183 
184 	/** @return true if the image of this element should be displayed in grey */
shouldBeGrey(Object element)185 	private boolean shouldBeGrey(Object element) {
186 		// It is grey if a MUIElement is not visible or not rendered
187 		// It is not grey if this is not a MUIElement or if it is rendered and
188 		// visible.
189 		return element instanceof MUIElement
190 				&& !(((MUIElement) element).isToBeRendered() && ((MUIElement) element).isVisible());
191 	}
192 
193 	/**
194 	 *
195 	 * @param key  the key of image (can be a constants from ResourceProvider or a
196 	 *             platform:/ uri location
197 	 * @param grey if true returns the grey version if original image exists
198 	 * @return the image with a give key or grey version.
199 	 */
getImage(String key, boolean grey)200 	private Image getImage(String key, boolean grey) {
201 
202 		// try to get image directly with right key and grey value
203 		Image result = getComponentImages().get(key + (grey ? GREY_SUFFIX : "")); //$NON-NLS-1$
204 
205 		// may be image not yet created
206 		if (result == null) {
207 			result = getComponentImages().get(key);
208 			// If no image found, ask the resource pool to create it...
209 			if (result == null && !key.startsWith("platform:")) { //$NON-NLS-1$
210 				try {
211 					result = createImage(key);
212 				} catch (final Exception e) {
213 				}
214 				if (result != null) {
215 					getComponentImages().put(key, result);
216 				}
217 			}
218 
219 			// Create the grey version of image and put it in registry
220 			if (result != null && grey) {
221 				final Image greyImg = new Image(result.getDevice(), result, SWT.IMAGE_GRAY);
222 				getComponentImages().put(key + GREY_SUFFIX, greyImg);
223 				result = greyImg;
224 			}
225 		}
226 		return result;
227 	}
228 
229 	/**
230 	 * Get image from an element Implements algorithm described in bug #465271
231 	 *
232 	 * @param element the Application Element
233 	 * @param key     the element image key if no icon URI
234 	 * @return Image or null if nothing found
235 	 */
getImage(Object element, String key)236 	public Image getImage(Object element, String key) {
237 		Image result = null;
238 
239 		if (element instanceof MUILabel) {
240 			result = getImageFromIconURI((MUILabel) element);
241 		}
242 
243 		if (result == null) {
244 			// This is a model element with a key or a MUILabel without IconUri
245 			final boolean greyVersion = shouldBeGrey(element);
246 			result = getImage(key, greyVersion);
247 		}
248 
249 		return result;
250 
251 	}
252 
253 	/**
254 	 * Create a readable ImageDescriptor behind URI.
255 	 *
256 	 * @param uri
257 	 * @return
258 	 */
getImageDescriptorFromUri(String uri)259 	private ImageDescriptor getImageDescriptorFromUri(String uri) {
260 		ImageDescriptor result = null;
261 
262 		URL url = findPlatformImage(uri);
263 
264 		if (url != null) {
265 			ImageDescriptor imageDesc = ImageDescriptor.createFromURL(url);
266 			Image scaled = Util.scaleImage(imageDesc.createImage(), MAX_IMG_SIZE);
267 			createdImages.add(scaled);
268 			result = ImageDescriptor.createFromImage(scaled);
269 		}
270 
271 		return result;
272 	}
273 
274 	@SuppressWarnings("resource")
findPlatformImage(String uri)275 	private static URL findPlatformImage(String uri) {
276 		// SEVERAL CASES are possible here :
277 		// * uri = platform:/plugin/myplugin/icons/image.gif
278 		// * uri = platform:/resource/myplugin/icons/image.gif
279 		// * uri : platform:/plugin/myplugin/$nl$/icons/image.gif
280 
281 		// We must check if file exists before creating the ImageDescriptor
282 		// because ImageRegistry will throw and print a DeviceResourceException
283 		// With the E4 editors, the platform:/plugin/ is set for
284 		// runtime, but the file can be in workspace during development In this
285 		// case, we must rather use platform:/resource/.
286 		// Used ideas from the ImageTooltip code around line 70 to fix this
287 
288 		InputStream stream = null;
289 		URL url = null;
290 
291 		try {
292 			final URL uri2url = new URL(uri);
293 			url = FileLocator.toFileURL(uri2url);
294 			stream = url.openStream();
295 		} catch (final IOException e) {
296 			// If no stream behind this URL, it is probably a platform:/plugin
297 			// which must be found as a platform:/resource (this case occurs in
298 			// the model editor when icon URI are set using the dialog)
299 			url = null;
300 			if (uri.startsWith("platform:/plugin")) //$NON-NLS-1$
301 			{
302 				try {
303 					// Try to get it using 'platform:/resource'
304 					final URL resUrl = new URL(uri.replace("platform:/plugin", "platform:/resource")); //$NON-NLS-1$//$NON-NLS-2$
305 					url = FileLocator.toFileURL(resUrl);
306 					stream = url.openStream();
307 
308 				} catch (final IOException e2) {
309 					// No file behind, may be this is a $nl$ or a $ws$ path..
310 					// must use find on FileLocator which does not deal with
311 					// platform:/resource !
312 					try {
313 						url = FileLocator.find(new URL(uri));
314 						stream = url != null ? url.openStream() : null;
315 					} catch (final IOException ex) {
316 						url = null;
317 						// Can't do more !
318 					}
319 				}
320 			}
321 		}
322 
323 		if (stream != null) {
324 			try {
325 				stream.close();
326 			} catch (final IOException ex) {
327 			}
328 		}
329 		return url;
330 	}
331 
getImage(Object element)332 	public Image getImage(Object element) {
333 		return null;
334 	}
335 
336 
getLabel(Object element)337 	public abstract String getLabel(Object element);
338 
getDetailLabel(Object element)339 	public abstract String getDetailLabel(Object element);
340 
getDescription(Object element)341 	public abstract String getDescription(Object element);
342 
getEditor(Composite parent, Object object)343 	public Composite getEditor(Composite parent, Object object) {
344 		if (generator != null) {
345 			generator.stopGenerating();
346 			generator = null;
347 		}
348 		editorControl = doGetEditor(parent, object);
349 		return editorControl;
350 	}
351 
doGetEditor(Composite parent, Object object)352 	protected abstract Composite doGetEditor(Composite parent, Object object);
353 
getChildList(Object element)354 	public abstract IObservableList<?> getChildList(Object element);
355 
getLabelProperties()356 	public FeaturePath[] getLabelProperties() {
357 		return new FeaturePath[] {};
358 	}
359 
getActions(Object element)360 	public List<Action> getActions(Object element) {
361 		return Collections.emptyList();
362 	}
363 
364 	/**
365 	 * Translates an input <code>String</code> using the current
366 	 * {@link ResourceBundleTranslationProvider} and <code>locale</code> from the
367 	 * {@link TranslationService}.
368 	 *
369 	 * @param string the string to translate, may not be null.
370 	 * @return the translated string or the input string if it could not be
371 	 *         translated.
372 	 */
translate(String string)373 	public String translate(String string) {
374 		return ControlFactory.tr(translationProvider, string);
375 	}
376 
377 	/**
378 	 * @param element
379 	 * @return the list of actions that are populated in the import menu. Can be
380 	 *         empty but is never null.
381 	 */
getActionsImport(Object element)382 	public List<Action> getActionsImport(Object element) {
383 		return Collections.emptyList();
384 	}
385 
getLocalizedLabel(MUILabel element)386 	protected String getLocalizedLabel(MUILabel element) {
387 		return ControlFactory.getLocalizedLabel(translationProvider, element);
388 	}
389 
isFocusChild(Control control)390 	private boolean isFocusChild(Control control) {
391 		Control c = control;
392 		while (c != null && c != editorControl) {
393 			c = c.getParent();
394 		}
395 		return c != null;
396 	}
397 
handleCopy()398 	public void handleCopy() {
399 		if (editorControl != null) {
400 			final Control focusControl = editorControl.getDisplay().getFocusControl();
401 
402 			if (isFocusChild(focusControl) && focusControl.getData(ControlFactory.COPY_HANDLER) != null) {
403 				((Handler) focusControl.getData(ControlFactory.COPY_HANDLER)).copy();
404 			}
405 		}
406 	}
407 
handlePaste()408 	public void handlePaste() {
409 		if (editorControl != null) {
410 			final Control focusControl = editorControl.getDisplay().getFocusControl();
411 
412 			if (isFocusChild(focusControl) && focusControl.getData(ControlFactory.COPY_HANDLER) != null) {
413 				((Handler) focusControl.getData(ControlFactory.COPY_HANDLER)).paste();
414 			}
415 		}
416 	}
417 
handleCut()418 	public void handleCut() {
419 		if (editorControl != null) {
420 			final Control focusControl = editorControl.getDisplay().getFocusControl();
421 
422 			if (isFocusChild(focusControl) && focusControl.getData(ControlFactory.COPY_HANDLER) != null) {
423 				((Handler) focusControl.getData(ControlFactory.COPY_HANDLER)).cut();
424 			}
425 		}
426 	}
427 
createScrollableContainer(Composite parent)428 	protected Composite createScrollableContainer(Composite parent) {
429 		final ScrolledComposite scrolling = new ScrolledComposite(parent, SWT.H_SCROLL | SWT.V_SCROLL);
430 		scrolling.setBackgroundMode(SWT.INHERIT_DEFAULT);
431 		scrolling.setData(CSS_CLASS_KEY, "formContainer"); //$NON-NLS-1$
432 
433 		final Composite contentContainer = new Composite(scrolling, SWT.NONE);
434 
435 		contentContainer.setData(CSS_CLASS_KEY, "formContainer"); //$NON-NLS-1$
436 		scrolling.setExpandHorizontal(true);
437 		scrolling.setExpandVertical(true);
438 		scrolling.setContent(contentContainer);
439 
440 		scrolling.addControlListener(new ControlAdapter() {
441 			@Override
442 			public void controlResized(ControlEvent e) {
443 				final Rectangle r = scrolling.getClientArea();
444 				scrolling.setMinSize(contentContainer.computeSize(r.width, SWT.DEFAULT));
445 			}
446 		});
447 
448 		scrolling.setLayoutData(new GridData(GridData.FILL_BOTH));
449 
450 		final GridLayout gl = new GridLayout(3, false);
451 		gl.horizontalSpacing = 10;
452 		contentContainer.setLayout(gl);
453 
454 		return contentContainer;
455 	}
456 
createContributedEditorTabs(CTabFolder folder, EMFDataBindingContext context, WritableValue<M> master, Class<? super M> clazz)457 	protected void createContributedEditorTabs(CTabFolder folder, EMFDataBindingContext context,
458 			WritableValue<M> master, Class<? super M> clazz) {
459 		final List<AbstractElementEditorContribution> contributionList = editor.getTabContributionsForClass(clazz);
460 
461 		for (final AbstractElementEditorContribution eec : contributionList) {
462 			final CTabItem item = new CTabItem(folder, SWT.BORDER);
463 			item.setText(eec.getTabLabel());
464 
465 			final Composite parent = createScrollableContainer(folder);
466 			item.setControl(parent.getParent());
467 
468 			eec.createContributedEditorTab(parent, context, master, getEditingDomain(), project);
469 		}
470 
471 	}
472 
473 	/**
474 	 * Generates an ID when the another field changes. Must be called after master
475 	 * is set with the objects value.
476 	 *
477 	 * @param attSource The source attribute
478 	 * @param attId     The id attribute to generate
479 	 * @param control   optional control to disable generator after losing focus or
480 	 *                  disposing
481 	 */
enableIdGenerator(EAttribute attSource, EAttribute attId, Control control)482 	protected void enableIdGenerator(EAttribute attSource, EAttribute attId, Control control) {
483 		if (generator != null) {
484 			generator.stopGenerating();
485 			generator = null;
486 		}
487 		if (getEditor().isAutoCreateElementId()) {
488 			generator = new IdGenerator();
489 			@SuppressWarnings("unchecked")
490 			IValueProperty<M, String> addSourceProp = EMFEditProperties.value(getEditingDomain(), attSource);
491 			@SuppressWarnings("unchecked")
492 			IValueProperty<M, String> attIdProp = EMFEditProperties.value(getEditingDomain(), attId);
493 			generator.bind(getMaster(), addSourceProp, attIdProp, control);
494 		}
495 	}
496 
497 	@PreDestroy
dispose()498 	public void dispose() {
499 		createdImages.stream().filter(Objects::nonNull).filter(i -> !i.isDisposed()).forEach(Image::dispose);
500 	}
501 
502 }
503