1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.g2d.gallery;
14 import java.awt.Color;
15 import java.awt.Component;
17 import java.awt.FontMetrics;
18 import java.awt.Paint;
19 import java.awt.Shape;
20 import java.awt.TexturePaint;
21 import java.awt.geom.Rectangle2D;
22 import java.awt.image.BufferedImage;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collections;
26 import java.util.HashSet;
27 import java.util.List;
29 import java.util.concurrent.Semaphore;
30 import java.util.concurrent.TimeUnit;
31 import java.util.function.Consumer;
33 import org.eclipse.jface.resource.FontRegistry;
34 import org.eclipse.jface.util.IPropertyChangeListener;
35 import org.eclipse.jface.util.PropertyChangeEvent;
36 import org.eclipse.jface.viewers.ContentViewer;
37 import org.eclipse.jface.viewers.ISelection;
38 import org.eclipse.jface.viewers.IStructuredContentProvider;
39 import org.eclipse.jface.viewers.IStructuredSelection;
40 import org.eclipse.jface.viewers.StructuredSelection;
41 import org.eclipse.jface.viewers.ViewerFilter;
42 import org.eclipse.swt.SWT;
43 import org.eclipse.swt.events.ControlEvent;
44 import org.eclipse.swt.events.ControlListener;
45 import org.eclipse.swt.graphics.FontData;
46 import org.eclipse.swt.graphics.Point;
47 import org.eclipse.swt.widgets.Composite;
48 import org.eclipse.swt.widgets.Control;
49 import org.eclipse.swt.widgets.Display;
50 import org.eclipse.swt.widgets.Event;
51 import org.eclipse.swt.widgets.Listener;
52 import org.simantics.g2d.canvas.Hints;
53 import org.simantics.g2d.canvas.ICanvasContext;
54 import org.simantics.g2d.canvas.impl.CanvasContext;
55 import org.simantics.g2d.chassis.SWTChassis;
56 import org.simantics.g2d.diagram.DiagramClass;
57 import org.simantics.g2d.diagram.DiagramHints;
58 import org.simantics.g2d.diagram.IDiagram;
59 import org.simantics.g2d.diagram.handler.DataElementMap;
60 import org.simantics.g2d.diagram.handler.PickRequest.PickPolicy;
61 import org.simantics.g2d.diagram.handler.layout.FlowLayout;
62 import org.simantics.g2d.diagram.impl.Diagram;
63 import org.simantics.g2d.diagram.participant.DiagramParticipant;
64 import org.simantics.g2d.diagram.participant.ElementInteractor;
65 import org.simantics.g2d.diagram.participant.ElementPainter;
66 import org.simantics.g2d.diagram.participant.Selection;
67 import org.simantics.g2d.diagram.participant.pointertool.PointerInteractor;
68 import org.simantics.g2d.dnd.IDragSourceParticipant;
69 import org.simantics.g2d.dnd.IDropTargetParticipant;
70 import org.simantics.g2d.element.ElementClass;
71 import org.simantics.g2d.element.ElementHints;
72 import org.simantics.g2d.element.ElementUtils;
73 import org.simantics.g2d.element.IElement;
74 import org.simantics.g2d.element.handler.Clickable;
75 import org.simantics.g2d.element.handler.Resize;
76 import org.simantics.g2d.element.handler.impl.DefaultTransform;
77 import org.simantics.g2d.element.handler.impl.Resizeable;
78 import org.simantics.g2d.element.handler.impl.TextImpl;
79 import org.simantics.g2d.element.impl.Element;
80 import org.simantics.g2d.image.Image;
81 import org.simantics.g2d.image.Image.Feature;
82 import org.simantics.g2d.image.Image.ImageListener;
83 import org.simantics.g2d.image.impl.Shadow.ShadowParameters;
84 import org.simantics.g2d.participant.BackgroundPainter;
85 import org.simantics.g2d.participant.KeyToCommand;
86 import org.simantics.g2d.participant.KeyUtil;
87 import org.simantics.g2d.participant.MouseUtil;
88 import org.simantics.g2d.participant.SymbolUtil;
89 import org.simantics.g2d.participant.TransformUtil;
90 import org.simantics.g2d.tooltip.TooltipParticipant;
91 import org.simantics.g2d.utils.FontHelper;
92 import org.simantics.scenegraph.g2d.G2DParentNode;
93 import org.simantics.scenegraph.g2d.events.command.CommandKeyBinding;
94 import org.simantics.scenegraph.g2d.nodes.ShapeNode;
95 import org.simantics.scenegraph.utils.GeometryUtils;
96 import org.simantics.scenegraph.utils.TextUtil;
97 import org.simantics.utils.datastructures.hints.IHintContext;
98 import org.simantics.utils.datastructures.hints.IHintContext.Key;
99 import org.simantics.utils.datastructures.hints.IHintContext.KeyOf;
100 import org.simantics.utils.threads.AWTThread;
101 import org.simantics.utils.threads.IThreadWorkQueue;
102 import org.simantics.utils.threads.SWTThread;
103 import org.simantics.utils.threads.ThreadUtils;
104 import org.simantics.utils.threads.logger.ITask;
105 import org.simantics.utils.threads.logger.ThreadLogger;
106 import org.simantics.utils.ui.SWTDPIUtil;
109 * @author Toni Kalajainen <toni.kalajainen@vtt.fi>
110 * @author Tuukka Lehtonen
112 public class GalleryViewer extends ContentViewer {
115 * A hint key for storing a GalleryViewer within the hint stack of
116 * {@link GalleryViewer}'s {@link ICanvasContext}.
118 public static final Key KEY_VIEWER = new KeyOf(GalleryViewer.class, "GALLERY_VIEWER");
120 public static final ShadowParameters SHADOW = new ShadowParameters(0.5, Color.BLACK, 5);
123 IThreadWorkQueue swtThread;
126 Component awtComponent;
129 GalleryItemPainter itemPainter;
132 Rectangle2D itemSize = new Rectangle2D.Double(0, 0, 50, 50);
133 Rectangle2D maxItemSize = itemSize.getBounds2D();
134 FlowLayout fl = new FlowLayout();
137 ElementClass itemClass;
138 FontRegistry fontRegistry;
139 Font currentItemFont;
141 GalleryTooltipProvider tooltipProvider = new GalleryTooltipProvider();
143 /** Background paint */
144 public static final Paint BG_PAINT;
146 // Create checkerboard pattern paint with 5x5 square size
147 BufferedImage bi = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB);
148 for (int x = 0; x < 2; ++x)
149 for (int y = 0; y < 2; ++y)
150 bi.setRGB(x, y, (((x ^ y) & 1) == 0) ? 0xffffffff : 0xfffdfdfd);
151 BG_PAINT = new TexturePaint(bi, new Rectangle2D.Double(0, 0, 10, 10));
152 //BG_PAINT = Color.WHITE;
155 public GalleryViewer(Composite composite) {
159 public GalleryViewer(Composite composite, int style) {
161 display = composite.getDisplay();
162 swtThread = SWTThread.getThreadAccess(composite);
163 chassis = new SWTChassis(composite, style) {
165 public Point computeSize(int wHint, int hHint, boolean changed) {
167 return super.computeSize(wHint, hHint, changed);
169 // Note: This code must take into account that FlowLayout expects to
170 // receive pixel coordinates, not SWT API coordinates.
174 // rect = ElementUtils.getSurroundingElementBoundsOnDiagram(diagram.getSnapshot());
178 Double wH = wHint==SWT.DEFAULT ? null : (double) SWTDPIUtil.upscaleSwt(wHint)-hMargin*2;
179 Double hH = hHint==SWT.DEFAULT ? null : (double) SWTDPIUtil.upscaleSwt(hHint)-vMargin*2;
180 rect = fl.computeSize(diagram, wH, hH);
181 SWTDPIUtil.downscaleSwt(rect, rect);
183 return new Point((int)rect.getWidth()+hMargin*2, (int)rect.getHeight()+vMargin*2);
187 // Create IDiagram for gallery
188 this.diagram = Diagram.spawnNew(DiagramClass.DEFAULT);
189 diagram.setHint(FlowLayout.HGAP, 5.0);
190 diagram.setHint(FlowLayout.VGAP, 5.0);
191 diagram.setHint(DiagramHints.KEY_ELEMENT_RASTER_TARGET_SIZE,
192 new java.awt.Point((int) itemSize.getWidth(), (int) itemSize.getHeight()));
194 // Create canvas context here in SWT thread but do not initialize it yet.
195 this.ctx = new CanvasContext(AWTThread.getThreadAccess());
197 chassis.populate(parameter -> {
198 awtComponent = parameter.getAWTComponent();
200 // Initialize the canvas context
201 ITask task = ThreadLogger.getInstance().begin("createCanvas");
202 initializeCanvasContext(ctx);
205 // Initialize canvas context hint context with KEY_VIEWER key.
206 IHintContext hintCtx = ctx.getDefaultHintContext();
207 hintCtx.setHint(KEY_VIEWER, GalleryViewer.this);
209 // Set IDiagram for canvas context
210 hintCtx.setHint(DiagramHints.KEY_DIAGRAM, diagram);
213 ThreadUtils.asyncExec(swtThread, () -> resized(false));
216 chassis.addControlListener(new ControlListener() {
218 public void controlMoved(ControlEvent e) {}
220 public void controlResized(ControlEvent e) {
224 ITask task2 = ThreadLogger.getInstance().begin("fonts");
226 this.fontRegistry = FontHelper.getCurrentThemeFontRegistry();
227 fontRegistry.addListener(fontRegistryListener);
228 currentItemFont = FontHelper.toAwt(fontRegistry, "org.simantics.gallery.itemfont");
229 } catch (IllegalStateException e) {
230 // No workbench available, use SWT control fonts for viewer items.
231 org.eclipse.swt.graphics.Font f = chassis.getFont();
232 currentItemFont = FontHelper.toAwt(f.getFontData()[0]);
237 itemClass = ElementClass.compile(
238 DefaultTransform.INSTANCE,
240 //TextAsTooltip.INSTANCE,
242 Resizeable.UNCONSTRICTED,
243 new GalleryItemSGNode(currentItemFont)
246 chassis.addListener(SWT.Dispose, new Listener() {
248 public void handleEvent(Event event) {
249 if (fontRegistry != null)
250 fontRegistry.removeListener(fontRegistryListener);
252 // Prevent memory leaks.
253 ThreadUtils.asyncExec(ctx.getThreadAccess(), () -> {
254 chassis.getAWTComponent().setCanvasContext(null);
261 IPropertyChangeListener fontRegistryListener = new IPropertyChangeListener() {
263 public void propertyChange(PropertyChangeEvent event) {
264 FontData fdn = ((FontData[]) event.getNewValue())[0];
265 //System.out.println(event.getSource() + ": font changed: " + event.getProperty() + ": " + fdn);
266 currentItemFont = FontHelper.toAwt(fdn);
267 itemClass.getSingleItem(GalleryItemSGNode.class).setFont(currentItemFont);
268 // FIXME: a bug exists in this case. The group size will not be refreshed even though the sizes of the gallery items are recalculated and changed.
269 ThreadUtils.asyncExec(swtThread, () -> resized(true));
274 * Set alignment of elements (left, right, center, fill)
277 public void setAlign(FlowLayout.Align align) {
278 diagram.setHint(FlowLayout.ALIGN, align);
283 * Sets the content filter of this viewer. The filter will be invoked after
284 * getting input data elements from the content provider and before setting
285 * the new content for the viewer. Any previously set filter will be
289 * This method will not refresh the viewer, this needs to be done separately
290 * using {@link #refresh()} or {@link #refresh(Consumer)}.
293 * @param filter the new filter
295 public void setFilter(ViewerFilter filter) {
296 if (filter == this.filter || (filter != null && filter.equals(this.filter)))
299 this.filter = filter;
302 public ViewerFilter getFilter() {
306 private void resized() {
310 private void resized(final boolean refreshElementSizes) {
311 //System.out.println(this + ".resized(" + refreshElementSizes + ")");
312 if (chassis.isDisposed())
314 org.eclipse.swt.graphics.Rectangle b = SWTDPIUtil.upscaleSwt(chassis.getBounds());
315 //System.out.println("chassis bounds: " + b);
316 final Rectangle2D bounds = new Rectangle2D.Double(hMargin, vMargin, b.width-hMargin*2, b.height-vMargin*2);
317 ctx.getThreadAccess().asyncExec(() -> {
318 if (ctx.isDisposed())
322 //System.out.println(this + ".resized(" + refreshElementSizes + ") AWT update");
323 if (refreshElementSizes)
324 refreshElementSizes();
325 fl.layout(diagram, bounds);
327 // Makes sure RTreeNode is marked dirty and everything is
328 // properly repainted.
329 if (itemPainter != null)
330 itemPainter.updateAll();
331 ctx.getContentContext().setDirty();
336 * Invoke only from AWT thread.
341 private void initializeCanvasContext(final CanvasContext canvasContext) {
342 // Create canvas context and a layer of interactors
343 final IHintContext h = canvasContext.getDefaultHintContext();
345 // Support & Util Participants
346 canvasContext.add( new TransformUtil() );
348 canvasContext.add( new MouseUtil() );
349 canvasContext.add( new KeyUtil() );
350 canvasContext.add( new SymbolUtil() );
352 // Grid & Ruler & Background
353 h.setHint(Hints.KEY_BACKGROUND_PAINT, BG_PAINT);
354 canvasContext.add( new BackgroundPainter() );
357 canvasContext.add( new KeyToCommand( CommandKeyBinding.DEFAULT_BINDINGS ) );
359 ////// Diagram Participants //////
360 PointerInteractor pi = new PointerInteractor(true, true, false, true, false, null);
361 pi.setBoxSelectMode(PickPolicy.PICK_INTERSECTING_OBJECTS);
362 canvasContext.add( pi );
363 canvasContext.add( selection = new Selection() );
364 canvasContext.add( new DiagramParticipant() );
365 canvasContext.add( itemPainter = new GalleryItemPainter() );
366 canvasContext.add( new ElementInteractor() );
367 canvasContext.add( new TooltipParticipant());
369 h.setHint(ElementPainter.KEY_SELECTION_FRAME_COLOR, Color.WHITE);
370 h.setHint(ElementPainter.KEY_SELECTION_CONTENT_COLOR, new Color(0.7f, 0.7f, 1.f, 0.5f));
371 h.setHint(Hints.KEY_TOOL, Hints.POINTERTOOL);
373 // Adds DragInteractor & DropInteractor
375 // therefore has to be done before assertParticipantDependencies.
376 // Also, this must be invoked BEFORE SWTChassis chassis.setCanvasContext
377 // because otherwise setCanvasContext would be ran in the
378 // wrong thread (SWT) for AWTChassis.
379 chassis.getAWTComponent().setCanvasContext(canvasContext);
381 swtThread.asyncExec(() -> {
382 if (!chassis.isDisposed())
383 chassis.setCanvasContext(canvasContext);
386 canvasContext.assertParticipantDependencies();
390 public Control getControl() {
395 protected void inputChanged(Object input, Object oldInput) {
396 // Skip automatic refreshing at setInput to allow room for manual
397 // optimization in the client.
402 public ISelection getSelection() {
403 Set<IElement> sel = selection.getSelection(0);
405 return StructuredSelection.EMPTY;
406 List<Object> elements = new ArrayList<Object>(sel.size());
407 for (IElement e : sel)
408 elements.add(e.getHint(ElementHints.KEY_OBJECT));
409 return new StructuredSelection(elements);
412 @SuppressWarnings("unchecked")
414 public void setSelection(ISelection selection, boolean reveal) {
415 List<Object> selectedObjects = Collections.EMPTY_LIST;
416 if (selection instanceof IStructuredSelection) {
417 selectedObjects = ((IStructuredSelection) selection).toList();
420 DataElementMap map = diagram.getDiagramClass().getSingleItem(DataElementMap.class);
421 List<IElement> selectionElements = new ArrayList<IElement>();
422 for (Object o : selectedObjects)
423 selectionElements.add( map.getElement(diagram, o) );
425 this.selection.setSelection(0, selectionElements);
429 * Refreshes this viewer completely with information freshly obtained from this
433 public void refresh() {
437 public Object[] getFilteredElements() {
438 IStructuredContentProvider cp = (IStructuredContentProvider) getContentProvider();
440 return new Object[0];
442 Object[] elements = cp.getElements( getInput() );
443 Object[] filtered = filter( elements );
447 protected Object[] filter(Object[] items) {
448 if (filter != null) {
449 ArrayList<Object> filtered = new ArrayList<Object>(items.length);
450 Object root = getInput();
451 for (int i = 0; i < items.length; i++) {
452 if (!filter.select(this, root, items[i]))
454 filtered.add(items[i]);
456 return filtered.toArray();
462 * @param contentCallback a callback for receiving the final filtered
463 * elements left visible
465 public void refresh(Consumer<Object[]> contentCallback) {
466 //System.out.println(this + ".refresh(" + contentCallback + ")");
467 Object[] elements = getFilteredElements();
468 refreshWithContent(elements);
469 if (contentCallback != null) {
470 contentCallback.accept(elements);
475 * Set viewer contents to the specified objects.
476 * This method must be invoked from the SWT thread.
480 public void refreshWithContent(final Object[] objects) {
481 if (!swtThread.currentThreadAccess())
482 throw new IllegalStateException("Not invoked from SWT thread");
484 //System.out.println(this + ".refreshWithContent(" + Arrays.toString(objects) + ")");
486 final Semaphore barrier = new Semaphore(0);
487 IThreadWorkQueue t = ctx.getThreadAccess();
488 ThreadUtils.asyncExec(t, new Runnable() {
498 public void perform() {
499 // $AWT-Thread-Begin$
500 //System.out.println(this + ".refreshWithContent(" + Arrays.toString(objects) + ") AWT WORK");
502 Object[] objectsCopy = Arrays.copyOf(objects, objects.length);
504 Set<Object> objs = new HashSet<Object>();
505 for (Object o : objectsCopy)
509 for (IElement e : diagram.getSnapshot()) {
510 Object backendObject = e.getHint(ElementHints.KEY_OBJECT);
511 if (!objs.remove(backendObject)) {
512 //System.out.println("Removing " + e);
513 diagram.removeElement(e);
516 for (int i = 0; i < objectsCopy.length; i++)
517 if (!objs.contains(objectsCopy[i]))
518 objectsCopy[i] = null;
520 // 2. Add new elements
521 for (Object o : objectsCopy) {
525 IElement e = Element.spawnNew(itemClass);
526 e.setHint(ElementHints.KEY_OBJECT, o);
527 // e.getElementClass().getSingleItem(Resize.class).resize(e, itemSize);
528 ILabelProvider lp = (ILabelProvider) getLabelProvider();
529 Image i = lp.getImage(o);
530 if (i.getFeatures().contains(Feature.Volatile))
531 i.addImageListener(imageListener);
532 e.setHint(GalleryItemSGNode.KEY_IMAGE, i);
535 String tooltipText = lp.getToolTipText(o);
536 java.awt.Image tooltipImage = lp.getToolTipImage(o);
537 if (tooltipText != null || tooltipImage != null) {
538 e.setHint(TooltipParticipant.TOOLTIP_KEY, tooltipProvider);
539 if (tooltipText != null)
540 e.setHint(GalleryTooltipProvider.TOOLTIP_TEXT, tooltipText);
541 if (tooltipImage != null)
542 e.setHint(GalleryTooltipProvider.TOOLTIP_IMAGE, tooltipImage);
544 diagram.addElement(e);
546 e.getElementClass().getSingleItem(GalleryItemSGNode.class).update(e);
548 // Image si = ImageUtils.createShadow(i, SHADOW, (int) itemSize.getWidth(), (int) itemSize.getHeight());
549 // si = ImageUtils.createBuffer(si);
550 // e.setHint(GalleryItemPainter.KEY_IMAGE_SHADOW, si);
552 ElementUtils.setText(e, lp.getText(o));
553 //System.out.println("Added: " + e);
556 // 3. Calculate maximum vertical space needed by current diagram element texts
557 refreshElementSizes();
559 ThreadUtils.asyncExec(swtThread, () -> resized(false));
564 boolean done = false;
567 done = barrier.tryAcquire(10, TimeUnit.MILLISECONDS);
568 while (!done && display.readAndDispatch()) {
569 done = barrier.tryAcquire();
571 } catch (InterruptedException e) {
578 * Invoke from canvas context thread only.
580 void refreshElementSizes() {
581 if (awtComponent == null) {
582 System.err.println("GalleryViewer.refreshElementSizes: awtComponent is null");
586 //System.out.println(this + ".refreshElementSizes()");
587 // Calculate maximum vertical space needed by current diagram element texts
588 FontMetrics metrics = awtComponent.getFontMetrics(currentItemFont);
589 int fontHeight = metrics.getHeight();
590 Rectangle2D size = SWTDPIUtil.upscaleSwt(itemSize);
591 int maxWidth = (int) size.getWidth();
592 java.awt.Point targetSize = new java.awt.Point((int) size.getWidth(), (int) size.getHeight());
593 diagram.setHint(DiagramHints.KEY_ELEMENT_RASTER_TARGET_SIZE, targetSize);
594 int maxLinesNeeded = 0;
595 for (IElement el : diagram.getElements()) {
596 // This makes it possible to give the gallery item a reference size
597 // for caching rendered images in the correct size only.
598 // NOTE: currently this is not used in GalleryItemPainter since the
599 // target size is now propagated through the element class loading
600 // process through the diagram hint KEY_ELEMENT_RASTER_TARGET_SIZE.
601 el.setHint(GalleryItemSGNode.KEY_TARGET_IMAGE_SIZE, targetSize);
603 String text = ElementUtils.getText(el);
604 int linesNeeded = TextUtil.wordWrap(text, metrics, maxWidth).length;
605 maxLinesNeeded = Math.max(maxLinesNeeded, linesNeeded);
606 int textSpaceNeeded = fontHeight * linesNeeded + metrics.getMaxDescent();
607 Rectangle2D s = new Rectangle2D.Double(0, 0, size.getWidth(), size.getHeight() + textSpaceNeeded);
608 el.getElementClass().getSingleItem(Resize.class).resize(el, s);
609 //System.out.println(this + " lines needed: " + linesNeeded + " = " + s);
611 el.getElementClass().getSingleItem(GalleryItemSGNode.class).update(el);
613 int maxTextSpaceNeeded = fontHeight * maxLinesNeeded + metrics.getMaxDescent();
614 maxItemSize = new Rectangle2D.Double(0, 0, size.getWidth(), size.getHeight() + maxTextSpaceNeeded);
615 //System.out.println(this + "[" + diagram.getElements().size() + "]: max lines needed: " + maxLinesNeeded + " = " + fontHeight*maxLinesNeeded + " pixels");
618 ImageListener imageListener = new ImageListener() {
620 public void onContentChangedNotification(Image image) {
621 //System.out.println(Thread.currentThread() + ": contentChanged(" + image + ")");
622 // Update the image of the element if the element is found.
623 for (final IElement el : diagram.getSnapshot()) {
624 Image i = GalleryItemSGNode.getImage(el);
628 ctx.getThreadAccess().asyncExec(() -> {
629 //System.out.println(Thread.currentThread() + ": update scene graph(" + el + ")");
630 // Update scene graph and repaint.
631 el.getElementClass().getSingleItem(GalleryItemSGNode.class).update(el);
632 ctx.getContentContext().setDirty();
639 public void addDropSupport(final IDropTargetParticipant p) {
640 if (ctx.getThreadAccess().currentThreadAccess()) {
643 ctx.getThreadAccess().asyncExec(() -> {
644 if (!ctx.isDisposed())
650 public void addDragSupport(final IDragSourceParticipant p) {
651 if (ctx.getThreadAccess().currentThreadAccess()) {
654 ctx.getThreadAccess().asyncExec(() -> {
655 if (!ctx.isDisposed())
661 public CanvasContext getCanvasContext() {
665 public IDiagram getDiagram() {
669 //private final static Color BG_COLOR = new Color(0.3f, 0.3f, 1.0f, 0.35f);
671 static class GalleryItemPainter extends ElementPainter {
673 public void paintSelectionFrame(G2DParentNode elementNode, G2DParentNode selectionNode, IElement e, Color color) {
674 final Shape outline = ElementUtils.getElementBoundsOnDiagram(e);
675 Rectangle2D bounds = outline.getBounds2D();
676 GeometryUtils.expandRectangle(bounds, 2, 2, 2, 2);
678 ShapeNode shapenode = selectionNode.getOrCreateNode("shape_"+e.hashCode(), ShapeNode.class);
679 shapenode.setShape(bounds);
680 shapenode.setColor(new Color(0.3f, 0.3f, 1.0f, 0.25f));
681 shapenode.setFill(true);
683 // Paint selection before anything else in elementNode
684 selectionNode.setZIndex(Integer.MIN_VALUE);