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