/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.g2d.gallery; import java.awt.Color; import java.awt.Component; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Paint; import java.awt.Shape; import java.awt.TexturePaint; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import org.eclipse.jface.resource.FontRegistry; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.jface.viewers.ContentViewer; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.ViewerFilter; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.simantics.g2d.canvas.Hints; import org.simantics.g2d.canvas.ICanvasContext; import org.simantics.g2d.canvas.impl.CanvasContext; import org.simantics.g2d.chassis.SWTChassis; import org.simantics.g2d.diagram.DiagramClass; import org.simantics.g2d.diagram.DiagramHints; import org.simantics.g2d.diagram.IDiagram; import org.simantics.g2d.diagram.handler.DataElementMap; import org.simantics.g2d.diagram.handler.PickRequest.PickPolicy; import org.simantics.g2d.diagram.handler.layout.FlowLayout; import org.simantics.g2d.diagram.impl.Diagram; import org.simantics.g2d.diagram.participant.DiagramParticipant; import org.simantics.g2d.diagram.participant.ElementInteractor; import org.simantics.g2d.diagram.participant.ElementPainter; import org.simantics.g2d.diagram.participant.Selection; import org.simantics.g2d.diagram.participant.pointertool.PointerInteractor; import org.simantics.g2d.dnd.IDragSourceParticipant; import org.simantics.g2d.dnd.IDropTargetParticipant; import org.simantics.g2d.element.ElementClass; import org.simantics.g2d.element.ElementHints; import org.simantics.g2d.element.ElementUtils; import org.simantics.g2d.element.IElement; import org.simantics.g2d.element.handler.Clickable; import org.simantics.g2d.element.handler.Resize; import org.simantics.g2d.element.handler.impl.DefaultTransform; import org.simantics.g2d.element.handler.impl.Resizeable; import org.simantics.g2d.element.handler.impl.TextImpl; import org.simantics.g2d.element.impl.Element; import org.simantics.g2d.image.Image; import org.simantics.g2d.image.Image.Feature; import org.simantics.g2d.image.Image.ImageListener; import org.simantics.g2d.image.impl.Shadow.ShadowParameters; import org.simantics.g2d.participant.BackgroundPainter; import org.simantics.g2d.participant.KeyToCommand; import org.simantics.g2d.participant.KeyUtil; import org.simantics.g2d.participant.MouseUtil; import org.simantics.g2d.participant.SymbolUtil; import org.simantics.g2d.participant.TransformUtil; import org.simantics.g2d.tooltip.TooltipParticipant; import org.simantics.g2d.utils.FontHelper; import org.simantics.scenegraph.g2d.G2DParentNode; import org.simantics.scenegraph.g2d.events.command.CommandKeyBinding; import org.simantics.scenegraph.g2d.nodes.ShapeNode; import org.simantics.scenegraph.utils.GeometryUtils; import org.simantics.scenegraph.utils.TextUtil; import org.simantics.utils.datastructures.hints.IHintContext; import org.simantics.utils.datastructures.hints.IHintContext.Key; import org.simantics.utils.datastructures.hints.IHintContext.KeyOf; import org.simantics.utils.threads.AWTThread; import org.simantics.utils.threads.IThreadWorkQueue; import org.simantics.utils.threads.SWTThread; import org.simantics.utils.threads.ThreadUtils; import org.simantics.utils.threads.logger.ITask; import org.simantics.utils.threads.logger.ThreadLogger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.simantics.utils.ui.SWTDPIUtil; /** * @author Toni Kalajainen * @author Tuukka Lehtonen */ public class GalleryViewer extends ContentViewer { private static final Logger LOGGER = LoggerFactory.getLogger(GalleryViewer.class); /** * A hint key for storing a GalleryViewer within the hint stack of * {@link GalleryViewer}'s {@link ICanvasContext}. */ public static final Key KEY_VIEWER = new KeyOf(GalleryViewer.class, "GALLERY_VIEWER"); public static final ShadowParameters SHADOW = new ShadowParameters(0.5, Color.BLACK, 5); ViewerFilter filter; IThreadWorkQueue swtThread; Display display; SWTChassis chassis; Component awtComponent; CanvasContext ctx; Selection selection; GalleryItemPainter itemPainter; IDiagram diagram; /** element size */ Rectangle2D itemSize = new Rectangle2D.Double(0, 0, 50, 50); Rectangle2D maxItemSize = itemSize.getBounds2D(); FlowLayout fl = new FlowLayout(); int hMargin = 5; int vMargin = 5; ElementClass itemClass; FontRegistry fontRegistry; Font currentItemFont; GalleryTooltipProvider tooltipProvider = new GalleryTooltipProvider(); /** Background paint */ public static final Paint BG_PAINT; static { // Create checkerboard pattern paint with 5x5 square size BufferedImage bi = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB); for (int x = 0; x < 2; ++x) for (int y = 0; y < 2; ++y) bi.setRGB(x, y, (((x ^ y) & 1) == 0) ? 0xffffffff : 0xfffdfdfd); BG_PAINT = new TexturePaint(bi, new Rectangle2D.Double(0, 0, 10, 10)); //BG_PAINT = Color.WHITE; } public GalleryViewer(Composite composite) { this(composite, 0); } public GalleryViewer(Composite composite, int style) { super(); display = composite.getDisplay(); swtThread = SWTThread.getThreadAccess(composite); chassis = new SWTChassis(composite, style) { @Override public Point computeSize(int wHint, int hHint, boolean changed) { if (diagram == null) return super.computeSize(wHint, hHint, changed); // Note: This code must take into account that FlowLayout expects to // receive pixel coordinates, not SWT API coordinates. Rectangle2D rect; // if (!changed) { // rect = ElementUtils.getSurroundingElementBoundsOnDiagram(diagram.getSnapshot()); // } // else { Double wH = wHint==SWT.DEFAULT ? null : (double) SWTDPIUtil.upscaleSwt(wHint)-hMargin*2; Double hH = hHint==SWT.DEFAULT ? null : (double) SWTDPIUtil.upscaleSwt(hHint)-vMargin*2; rect = fl.computeSize(diagram, wH, hH); SWTDPIUtil.downscaleSwt(rect, rect); } return new Point((int)rect.getWidth()+hMargin*2, (int)rect.getHeight()+vMargin*2); } }; // Create IDiagram for gallery this.diagram = Diagram.spawnNew(DiagramClass.DEFAULT); diagram.setHint(FlowLayout.HGAP, 5.0); diagram.setHint(FlowLayout.VGAP, 5.0); diagram.setHint(DiagramHints.KEY_ELEMENT_RASTER_TARGET_SIZE, new java.awt.Point((int) itemSize.getWidth(), (int) itemSize.getHeight())); // Create canvas context here in SWT thread but do not initialize it yet. this.ctx = new CanvasContext(AWTThread.getThreadAccess()); chassis.populate(parameter -> { awtComponent = parameter.getAWTComponent(); // Initialize the canvas context ITask task = ThreadLogger.getInstance().begin("createCanvas"); initializeCanvasContext(ctx); task.finish(); // Initialize canvas context hint context with KEY_VIEWER key. IHintContext hintCtx = ctx.getDefaultHintContext(); hintCtx.setHint(KEY_VIEWER, GalleryViewer.this); // Set IDiagram for canvas context hintCtx.setHint(DiagramHints.KEY_DIAGRAM, diagram); // Force layout ThreadUtils.asyncExec(swtThread, () -> resized(false)); }); chassis.addControlListener(new ControlListener() { @Override public void controlMoved(ControlEvent e) {} @Override public void controlResized(ControlEvent e) { resized(); }}); ITask task2 = ThreadLogger.getInstance().begin("fonts"); try { this.fontRegistry = FontHelper.getCurrentThemeFontRegistry(); fontRegistry.addListener(fontRegistryListener); currentItemFont = FontHelper.toAwt(fontRegistry, "org.simantics.gallery.itemfont"); } catch (IllegalStateException e) { // No workbench available, use SWT control fonts for viewer items. org.eclipse.swt.graphics.Font f = chassis.getFont(); currentItemFont = FontHelper.toAwt(f.getFontData()[0]); } task2.finish(); itemClass = ElementClass.compile( DefaultTransform.INSTANCE, TextImpl.INSTANCE, //TextAsTooltip.INSTANCE, Clickable.INSTANCE, Resizeable.UNCONSTRICTED, new GalleryItemSGNode(currentItemFont) ); chassis.addListener(SWT.Dispose, new Listener() { @Override public void handleEvent(Event event) { if (fontRegistry != null) fontRegistry.removeListener(fontRegistryListener); // Prevent memory leaks. ThreadUtils.asyncExec(ctx.getThreadAccess(), () -> { chassis.getAWTComponent().setCanvasContext(null); ctx.dispose(); }); } }); } IPropertyChangeListener fontRegistryListener = new IPropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { FontData fdn = ((FontData[]) event.getNewValue())[0]; //System.out.println(event.getSource() + ": font changed: " + event.getProperty() + ": " + fdn); currentItemFont = FontHelper.toAwt(fdn); itemClass.getSingleItem(GalleryItemSGNode.class).setFont(currentItemFont); // 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. ThreadUtils.asyncExec(swtThread, () -> resized(true)); } }; /** * Set alignment of elements (left, right, center, fill) * @param align */ public void setAlign(FlowLayout.Align align) { diagram.setHint(FlowLayout.ALIGN, align); resized(); } /** * Sets the content filter of this viewer. The filter will be invoked after * getting input data elements from the content provider and before setting * the new content for the viewer. Any previously set filter will be * replaced. * *

* This method will not refresh the viewer, this needs to be done separately * using {@link #refresh()} or {@link #refresh(Consumer)}. *

* * @param filter the new filter */ public void setFilter(ViewerFilter filter) { if (filter == this.filter || (filter != null && filter.equals(this.filter))) return; this.filter = filter; } public ViewerFilter getFilter() { return filter; } private void resized() { resized(false); } private void resized(final boolean refreshElementSizes) { //System.out.println(this + ".resized(" + refreshElementSizes + ")"); if (chassis.isDisposed()) return; org.eclipse.swt.graphics.Rectangle b = SWTDPIUtil.upscaleSwt(chassis.getBounds()); //System.out.println("chassis bounds: " + b); final Rectangle2D bounds = new Rectangle2D.Double(hMargin, vMargin, b.width-hMargin*2, b.height-vMargin*2); ctx.getThreadAccess().asyncExec(() -> { if (ctx.isDisposed()) return; if (diagram == null) return; //System.out.println(this + ".resized(" + refreshElementSizes + ") AWT update"); if (refreshElementSizes) refreshElementSizes(); fl.layout(diagram, bounds); // Makes sure RTreeNode is marked dirty and everything is // properly repainted. if (itemPainter != null) itemPainter.updateAll(); ctx.getContentContext().setDirty(); }); } /** * Invoke only from AWT thread. * * @param thread * @return */ private void initializeCanvasContext(final CanvasContext canvasContext) { // Create canvas context and a layer of interactors final IHintContext h = canvasContext.getDefaultHintContext(); // Support & Util Participants canvasContext.add( new TransformUtil() ); canvasContext.add( new MouseUtil() ); canvasContext.add( new KeyUtil() ); canvasContext.add( new SymbolUtil() ); // Grid & Ruler & Background h.setHint(Hints.KEY_BACKGROUND_PAINT, BG_PAINT); canvasContext.add( new BackgroundPainter() ); // Key bindings canvasContext.add( new KeyToCommand( CommandKeyBinding.DEFAULT_BINDINGS ) ); ////// Diagram Participants ////// PointerInteractor pi = new PointerInteractor(true, true, false, true, false, null); pi.setBoxSelectMode(PickPolicy.PICK_INTERSECTING_OBJECTS); canvasContext.add( pi ); canvasContext.add( selection = new Selection() ); canvasContext.add( new DiagramParticipant() ); canvasContext.add( itemPainter = new GalleryItemPainter() ); canvasContext.add( new ElementInteractor() ); canvasContext.add( new TooltipParticipant()); h.setHint(ElementPainter.KEY_SELECTION_FRAME_COLOR, Color.WHITE); h.setHint(ElementPainter.KEY_SELECTION_CONTENT_COLOR, new Color(0.7f, 0.7f, 1.f, 0.5f)); h.setHint(Hints.KEY_TOOL, Hints.POINTERTOOL); // Adds DragInteractor & DropInteractor // therefore has to be done before assertParticipantDependencies. // Also, this must be invoked BEFORE SWTChassis chassis.setCanvasContext // because otherwise setCanvasContext would be ran in the // wrong thread (SWT) for AWTChassis. chassis.getAWTComponent().setCanvasContext(canvasContext); swtThread.asyncExec(() -> { if (!chassis.isDisposed()) chassis.setCanvasContext(canvasContext); }); canvasContext.assertParticipantDependencies(); } @Override public Control getControl() { return chassis; } @Override protected void inputChanged(Object input, Object oldInput) { // Skip automatic refreshing at setInput to allow room for manual // optimization in the client. //refresh(); } @Override public ISelection getSelection() { Set sel = selection.getSelection(0); if (sel.isEmpty()) return StructuredSelection.EMPTY; List elements = new ArrayList(sel.size()); for (IElement e : sel) elements.add(e.getHint(ElementHints.KEY_OBJECT)); return new StructuredSelection(elements); } @SuppressWarnings("unchecked") @Override public void setSelection(ISelection selection, boolean reveal) { List selectedObjects = Collections.EMPTY_LIST; if (selection instanceof IStructuredSelection) { selectedObjects = ((IStructuredSelection) selection).toList(); } DataElementMap map = diagram.getDiagramClass().getSingleItem(DataElementMap.class); List selectionElements = new ArrayList(); for (Object o : selectedObjects) selectionElements.add( map.getElement(diagram, o) ); this.selection.setSelection(0, selectionElements); } /** * Refreshes this viewer completely with information freshly obtained from this * viewer's model. */ @Override public void refresh() { refresh(null); } public Object[] getFilteredElements() { IStructuredContentProvider cp = (IStructuredContentProvider) getContentProvider(); if (cp == null) return new Object[0]; Object[] elements = cp.getElements( getInput() ); Object[] filtered = filter( elements ); return filtered; } protected Object[] filter(Object[] items) { if (filter != null) { ArrayList filtered = new ArrayList(items.length); Object root = getInput(); for (int i = 0; i < items.length; i++) { if (!filter.select(this, root, items[i])) continue; filtered.add(items[i]); } return filtered.toArray(); } return items; } /** * @param contentCallback a callback for receiving the final filtered * elements left visible */ public void refresh(Consumer contentCallback) { //System.out.println(this + ".refresh(" + contentCallback + ")"); Object[] elements = getFilteredElements(); refreshWithContent(elements); if (contentCallback != null) { contentCallback.accept(elements); } } /** * Set viewer contents to the specified objects. * This method must be invoked from the SWT thread. * * @param objects */ public void refreshWithContent(final Object[] objects) { if (!swtThread.currentThreadAccess()) throw new IllegalStateException("Not invoked from SWT thread"); //System.out.println(this + ".refreshWithContent(" + Arrays.toString(objects) + ")"); final Semaphore barrier = new Semaphore(0); IThreadWorkQueue t = ctx.getThreadAccess(); ThreadUtils.asyncExec(t, new Runnable() { @Override public void run() { try { perform(); } finally { barrier.release(); } } public void perform() { // $AWT-Thread-Begin$ //System.out.println(this + ".refreshWithContent(" + Arrays.toString(objects) + ") AWT WORK"); Object[] objectsCopy = Arrays.copyOf(objects, objects.length); Set objs = new HashSet(); for (Object o : objectsCopy) objs.add(o); // 1. Remove unused for (IElement e : diagram.getSnapshot()) { Object backendObject = e.getHint(ElementHints.KEY_OBJECT); if (!objs.remove(backendObject)) { //System.out.println("Removing " + e); diagram.removeElement(e); } } for (int i = 0; i < objectsCopy.length; i++) if (!objs.contains(objectsCopy[i])) objectsCopy[i] = null; // 2. Add new elements for (Object o : objectsCopy) { if (o == null) continue; IElement e = Element.spawnNew(itemClass); e.setHint(ElementHints.KEY_OBJECT, o); // e.getElementClass().getSingleItem(Resize.class).resize(e, itemSize); ILabelProvider lp = (ILabelProvider) getLabelProvider(); Image i = lp.getImage(o); if (i.getFeatures().contains(Feature.Volatile)) i.addImageListener(imageListener); e.setHint(GalleryItemSGNode.KEY_IMAGE, i); // tooltips String tooltipText = lp.getToolTipText(o); java.awt.Image tooltipImage = lp.getToolTipImage(o); if (tooltipText != null || tooltipImage != null) { e.setHint(TooltipParticipant.TOOLTIP_KEY, tooltipProvider); if (tooltipText != null) e.setHint(GalleryTooltipProvider.TOOLTIP_TEXT, tooltipText); if (tooltipImage != null) e.setHint(GalleryTooltipProvider.TOOLTIP_IMAGE, tooltipImage); } diagram.addElement(e); e.getElementClass().getSingleItem(GalleryItemSGNode.class).update(e); // Image si = ImageUtils.createShadow(i, SHADOW, (int) itemSize.getWidth(), (int) itemSize.getHeight()); // si = ImageUtils.createBuffer(si); // e.setHint(GalleryItemPainter.KEY_IMAGE_SHADOW, si); ElementUtils.setText(e, lp.getText(o)); //System.out.println("Added: " + e); } // 3. Calculate maximum vertical space needed by current diagram element texts refreshElementSizes(); ThreadUtils.asyncExec(swtThread, () -> resized(false)); // $AWT-Thread-End$ } }); boolean done = false; while (!done) { try { done = barrier.tryAcquire(10, TimeUnit.MILLISECONDS); while (!done && display.readAndDispatch()) { done = barrier.tryAcquire(); } } catch (InterruptedException e) { done = true; } } } /** * Invoke from canvas context thread only. */ void refreshElementSizes() { if (awtComponent == null) { LOGGER.error("GalleryViewer.refreshElementSizes: awtComponent is null"); return; } //System.out.println(this + ".refreshElementSizes()"); // Calculate maximum vertical space needed by current diagram element texts FontMetrics metrics = awtComponent.getFontMetrics(currentItemFont); int fontHeight = metrics.getHeight(); Rectangle2D size = SWTDPIUtil.upscaleSwt(itemSize); int maxWidth = (int) size.getWidth(); java.awt.Point targetSize = new java.awt.Point((int) size.getWidth(), (int) size.getHeight()); diagram.setHint(DiagramHints.KEY_ELEMENT_RASTER_TARGET_SIZE, targetSize); int maxLinesNeeded = 0; for (IElement el : diagram.getElements()) { // This makes it possible to give the gallery item a reference size // for caching rendered images in the correct size only. // NOTE: currently this is not used in GalleryItemPainter since the // target size is now propagated through the element class loading // process through the diagram hint KEY_ELEMENT_RASTER_TARGET_SIZE. el.setHint(GalleryItemSGNode.KEY_TARGET_IMAGE_SIZE, targetSize); String text = ElementUtils.getText(el); int linesNeeded = TextUtil.wordWrap(text, metrics, maxWidth).length; maxLinesNeeded = Math.max(maxLinesNeeded, linesNeeded); int textSpaceNeeded = fontHeight * linesNeeded + metrics.getMaxDescent(); Rectangle2D s = new Rectangle2D.Double(0, 0, size.getWidth(), size.getHeight() + textSpaceNeeded); el.getElementClass().getSingleItem(Resize.class).resize(el, s); //System.out.println(this + " lines needed: " + linesNeeded + " = " + s); el.getElementClass().getSingleItem(GalleryItemSGNode.class).update(el); } int maxTextSpaceNeeded = fontHeight * maxLinesNeeded + metrics.getMaxDescent(); maxItemSize = new Rectangle2D.Double(0, 0, size.getWidth(), size.getHeight() + maxTextSpaceNeeded); //System.out.println(this + "[" + diagram.getElements().size() + "]: max lines needed: " + maxLinesNeeded + " = " + fontHeight*maxLinesNeeded + " pixels"); } ImageListener imageListener = new ImageListener() { @Override public void onContentChangedNotification(Image image) { //System.out.println(Thread.currentThread() + ": contentChanged(" + image + ")"); // Update the image of the element if the element is found. for (final IElement el : diagram.getSnapshot()) { Image i = GalleryItemSGNode.getImage(el); if (image != i) continue; ctx.getThreadAccess().asyncExec(() -> { //System.out.println(Thread.currentThread() + ": update scene graph(" + el + ")"); // Update scene graph and repaint. el.getElementClass().getSingleItem(GalleryItemSGNode.class).update(el); ctx.getContentContext().setDirty(); }); break; } } }; public void addDropSupport(final IDropTargetParticipant p) { if (ctx.getThreadAccess().currentThreadAccess()) { ctx.add(p); } else { ctx.getThreadAccess().asyncExec(() -> { if (!ctx.isDisposed()) ctx.add(p); }); } } public void addDragSupport(final IDragSourceParticipant p) { if (ctx.getThreadAccess().currentThreadAccess()) { ctx.add(p); } else { ctx.getThreadAccess().asyncExec(() -> { if (!ctx.isDisposed()) ctx.add(p); }); } } public CanvasContext getCanvasContext() { return ctx; } public IDiagram getDiagram() { return diagram; } //private final static Color BG_COLOR = new Color(0.3f, 0.3f, 1.0f, 0.35f); static class GalleryItemPainter extends ElementPainter { @Override public void paintSelectionFrame(int selectionId, G2DParentNode elementNode, G2DParentNode selectionNode, IElement e, Color color) { final Shape outline = ElementUtils.getElementBoundsOnDiagram(e); Rectangle2D bounds = outline.getBounds2D(); GeometryUtils.expandRectangle(bounds, 2, 2, 2, 2); ShapeNode shapenode = selectionNode.getOrCreateNode("shape_"+e.hashCode(), ShapeNode.class); shapenode.setShape(bounds); shapenode.setColor(new Color(0.3f, 0.3f, 1.0f, 0.25f)); shapenode.setFill(true); // Paint selection before anything else in elementNode selectionNode.setZIndex(Integer.MIN_VALUE); } } }