X-Git-Url: https://gerrit.simantics.org/r/gitweb?p=simantics%2Fplatform.git;a=blobdiff_plain;f=bundles%2Forg.simantics.diagram%2Fsrc%2Forg%2Fsimantics%2Fdiagram%2Fsymbollibrary%2Fui%2FSymbolLibraryComposite.java;h=31c091fd4eed1041e9b869639463f0ec5e4adfbb;hp=3e515a521092e51816cb13185a9399f11e25505c;hb=86617be247efb3b904b3180d0569049aff232d75;hpb=969bd23cab98a79ca9101af33334000879fb60c5 diff --git a/bundles/org.simantics.diagram/src/org/simantics/diagram/symbollibrary/ui/SymbolLibraryComposite.java b/bundles/org.simantics.diagram/src/org/simantics/diagram/symbollibrary/ui/SymbolLibraryComposite.java index 3e515a521..31c091fd4 100644 --- a/bundles/org.simantics.diagram/src/org/simantics/diagram/symbollibrary/ui/SymbolLibraryComposite.java +++ b/bundles/org.simantics.diagram/src/org/simantics/diagram/symbollibrary/ui/SymbolLibraryComposite.java @@ -1,1429 +1,1428 @@ -/******************************************************************************* - * 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.diagram.symbollibrary.ui; - -import java.awt.datatransfer.StringSelection; -import java.awt.datatransfer.Transferable; -import java.awt.dnd.DnDConstants; -import java.awt.dnd.DragGestureEvent; -import java.awt.dnd.DragSourceDragEvent; -import java.awt.dnd.DragSourceDropEvent; -import java.awt.dnd.DragSourceEvent; -import java.lang.ref.SoftReference; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.WeakHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Semaphore; -import java.util.concurrent.SynchronousQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.eclipse.core.runtime.IAdaptable; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.jface.layout.GridDataFactory; -import org.eclipse.jface.layout.GridLayoutFactory; -import org.eclipse.jface.resource.FontDescriptor; -import org.eclipse.jface.resource.JFaceResources; -import org.eclipse.jface.resource.LocalResourceManager; -import org.eclipse.jface.viewers.AcceptAllFilter; -import org.eclipse.jface.viewers.BaseLabelProvider; -import org.eclipse.jface.viewers.IFilter; -import org.eclipse.jface.viewers.ISelection; -import org.eclipse.jface.viewers.IStructuredContentProvider; -import org.eclipse.jface.viewers.StructuredSelection; -import org.eclipse.jface.viewers.Viewer; -import org.eclipse.jface.viewers.ViewerFilter; -import org.eclipse.nebula.widgets.pgroup.PGroup; -import org.eclipse.swt.SWT; -import org.eclipse.swt.custom.ScrolledComposite; -import org.eclipse.swt.events.ControlAdapter; -import org.eclipse.swt.events.ControlEvent; -import org.eclipse.swt.events.DisposeEvent; -import org.eclipse.swt.events.DisposeListener; -import org.eclipse.swt.events.ExpandEvent; -import org.eclipse.swt.events.ExpandListener; -import org.eclipse.swt.events.ModifyEvent; -import org.eclipse.swt.events.ModifyListener; -import org.eclipse.swt.graphics.Color; -import org.eclipse.swt.graphics.Point; -import org.eclipse.swt.graphics.Rectangle; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Control; -import org.eclipse.swt.widgets.Event; -import org.eclipse.swt.widgets.Listener; -import org.eclipse.swt.widgets.Widget; -import org.simantics.db.ReadGraph; -import org.simantics.db.Resource; -import org.simantics.db.common.procedure.adapter.ListenerAdapter; -import org.simantics.db.common.request.UnaryRead; -import org.simantics.db.exception.DatabaseException; -import org.simantics.diagram.internal.Activator; -import org.simantics.diagram.symbolcontribution.CompositeSymbolGroup; -import org.simantics.diagram.symbolcontribution.IIdentifiedObject; -import org.simantics.diagram.symbolcontribution.ISymbolProvider; -import org.simantics.diagram.symbolcontribution.IdentifiedObject; -import org.simantics.diagram.symbolcontribution.SymbolProviderFactory; -import org.simantics.diagram.symbollibrary.IModifiableSymbolGroup; -import org.simantics.diagram.symbollibrary.ISymbolGroup; -import org.simantics.diagram.symbollibrary.ISymbolGroupListener; -import org.simantics.diagram.symbollibrary.ISymbolItem; -import org.simantics.diagram.symbollibrary.ui.FilterConfiguration.Mode; -import org.simantics.diagram.synchronization.ErrorHandler; -import org.simantics.diagram.synchronization.LogErrorHandler; -import org.simantics.diagram.synchronization.SynchronizationHints; -import org.simantics.g2d.canvas.Hints; -import org.simantics.g2d.canvas.ICanvasContext; -import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency; -import org.simantics.g2d.canvas.impl.DependencyReflection.Reference; -import org.simantics.g2d.chassis.AWTChassis; -import org.simantics.g2d.diagram.DiagramUtils; -import org.simantics.g2d.diagram.handler.PickContext; -import org.simantics.g2d.diagram.handler.PickRequest; -import org.simantics.g2d.diagram.handler.layout.FlowLayout; -import org.simantics.g2d.diagram.participant.AbstractDiagramParticipant; -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.element.ElementClass; -import org.simantics.g2d.element.ElementHints; -import org.simantics.g2d.element.IElement; -import org.simantics.g2d.element.handler.StaticSymbol; -import org.simantics.g2d.event.adapter.SWTMouseEventAdapter; -import org.simantics.g2d.gallery.GalleryViewer; -import org.simantics.g2d.gallery.ILabelProvider; -import org.simantics.g2d.image.DefaultImages; -import org.simantics.g2d.image.Image; -import org.simantics.g2d.image.Image.Feature; -import org.simantics.g2d.image.impl.ImageProxy; -import org.simantics.g2d.participant.TransformUtil; -import org.simantics.scenegraph.g2d.events.EventTypes; -import org.simantics.scenegraph.g2d.events.IEventHandler; -import org.simantics.scenegraph.g2d.events.MouseEvent; -import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent; -import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin; -import org.simantics.scl.runtime.tuple.Tuple2; -import org.simantics.ui.SimanticsUI; -import org.simantics.ui.dnd.LocalObjectTransfer; -import org.simantics.ui.dnd.LocalObjectTransferable; -import org.simantics.ui.dnd.MultiTransferable; -import org.simantics.ui.dnd.PlaintextTransfer; -import org.simantics.utils.datastructures.cache.ProvisionException; -import org.simantics.utils.datastructures.hints.IHintContext; -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.ui.ErrorLogger; -import org.simantics.utils.ui.ExceptionUtils; - -/** - * @author Tuukka Lehtonen - */ -public class SymbolLibraryComposite extends Composite { - - private static final int FILTER_DELAY = 500; - - private static final String KEY_VIEWER_INITIALIZED = "viewer.initialized"; - private static final String KEY_USER_EXPANDED = "userExpanded"; - private static final String KEY_GROUP_FILTERED = "groupFiltered"; - - /** Root composite */ - ScrolledComposite sc; - Composite c; - IThreadWorkQueue swtThread; - boolean defaultExpanded = false; - ISymbolProvider symbolProvider; - AtomicBoolean disposed = new AtomicBoolean(false); - - /** - * This value is incremented each time a load method is called and symbol - * group population is started. It can be used by - * {@link #populateGroups(ExecutorService, Control, Iterator, IFilter)} to - * tell whether it should stop its population job because a later load - * will override its results anyway. - */ - AtomicInteger loadCount = new AtomicInteger(); - - Map groups = new HashMap<>(); - Map groupViewers = new HashMap<>(); - Map expandedGroups = new HashMap<>(); - LocalResourceManager resourceManager; - FilterArea filter; - - ThreadFactory threadFactory = new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r, "Symbol Library Loader"); - t.setDaemon(false); - t.setPriority(Thread.NORM_PRIORITY); - return t; - } - }; - - Semaphore loaderSemaphore = new Semaphore(1); - ExecutorService loaderExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, - 2L, TimeUnit.SECONDS, - new SynchronousQueue(), - threadFactory); - - /** - * Used to prevent annoying reloading of symbols when groups are closed and - * reopened by not always having to schedule an {@link ImageLoader} in - * {@link LabelProvider#getImage(Object)}. - */ - Map> imageCache = new WeakHashMap>(); - - static final Pattern ANY = Pattern.compile(".*"); - Pattern currentFilterPattern = ANY; - - FilterConfiguration config = new FilterConfiguration(); - IFilter currentGroupFilter = AcceptAllFilter.getInstance(); - - ErrorHandler errorHandler = LogErrorHandler.INSTANCE; - - static class GroupDescriptor { - public final ISymbolGroup lib; - public final String label; - public final String description; - public final PGroup group; - - public GroupDescriptor(ISymbolGroup lib, String label, String description, PGroup group) { - assert(lib != null); - assert(label != null); - this.lib = lib; - this.label = label; - this.description = description; - this.group = group; - } - } - - Comparator groupComparator = new Comparator() { - @Override - public int compare(GroupDescriptor o1, GroupDescriptor o2) { - return o1.label.compareToIgnoreCase(o2.label); - } - }; - - static final EnumSet VOLATILE = EnumSet.of(Feature.Volatile); - - static class PendingImage extends ImageProxy { - EnumSet features; - PendingImage(Image source, EnumSet features) { - super(source); - this.features = features; - } - @Override - public EnumSet getFeatures() { - return features; - } - } - - class LabelProvider extends BaseLabelProvider implements ILabelProvider { - @Override - public Image getImage(final Object element) { - ISymbolItem item = (ISymbolItem) element; - // Use a volatile ImageProxy to make the image loading asynchronous. - ImageProxy proxy = null; - SoftReference proxyRef = imageCache.get(item); - if (proxyRef != null) - proxy = proxyRef.get(); - if (proxy == null) { - proxy = new PendingImage(DefaultImages.HOURGLASS.get(), VOLATILE); - imageCache.put(item, new SoftReference(proxy)); - ThreadUtils.getNonBlockingWorkExecutor().schedule(new ImageLoader(proxy, item), 100, TimeUnit.MILLISECONDS); - } - return proxy; - } - @Override - public String getText(final Object element) { - return ((ISymbolItem) element).getName(); - } - @Override - public String getToolTipText(Object element) { - ISymbolItem item = (ISymbolItem) element; - String name = item.getName(); - String desc = item.getDescription(); - return name.equals(desc) ? name : name + " - " + desc; - } - - @Override - public java.awt.Image getToolTipImage(Object object) { - return null; - } - @Override - public Color getToolTipBackgroundColor(Object object) { - return null; - } - - @Override - public Color getToolTipForegroundColor(Object object) { - return null; - } - } - - public SymbolLibraryComposite(final Composite parent, int style, SymbolProviderFactory symbolProvider) { - super(parent, style); - init(parent, style); - SimanticsUI.getSession().asyncRequest(new CreateSymbolProvider(symbolProvider), new SymbolProviderListener()); - addDisposeListener(new DisposeListener() { - @Override - public void widgetDisposed(DisposeEvent e) { - disposed.set(true); - } - }); - } - - /** - * - */ - static class CreateSymbolProvider extends UnaryRead { - public CreateSymbolProvider(SymbolProviderFactory factory) { - super(factory); - } - @Override - public ISymbolProvider perform(ReadGraph graph) throws DatabaseException { - //System.out.println("CreateSymbolProvider.perform: " + parameter); - ISymbolProvider provider = parameter.create(graph); - //print(provider); - return provider; - } - } - - @SuppressWarnings("unused") - private static void print(ISymbolProvider provider) { - for (ISymbolGroup grp : provider.getSymbolGroups()) { - System.out.println("GROUP: " + grp); - if (grp instanceof CompositeSymbolGroup) { - CompositeSymbolGroup cgrp = (CompositeSymbolGroup) grp; - for (ISymbolGroup grp2 : cgrp.getGroups()) { - System.out.println("\tGROUP: " + grp2); - } - } - } - } - - /** - * - */ - class SymbolProviderListener extends ListenerAdapter { - @Override - public void exception(Throwable t) { - ErrorLogger.defaultLogError(t); - } - @Override - public void execute(ISymbolProvider result) { - //System.out.println("SymbolProviderListener: " + result); - symbolProvider = result; - if (result != null) { - Collection groups = result.getSymbolGroups(); - //print(result); - load(groups); - } - } - public boolean isDisposed() { - boolean result = SymbolLibraryComposite.this.isDisposed(); - return result; - } - } - - private void init(final Composite parent, int style) { - GridLayoutFactory.fillDefaults().spacing(0,0).applyTo(this); -// setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_RED)); - - this.resourceManager = new LocalResourceManager(JFaceResources.getResources(getDisplay()), this); - swtThread = SWTThread.getThreadAccess(this); - - filter = new FilterArea(this, SWT.NONE); - GridDataFactory.fillDefaults().grab(true, false).applyTo(filter); - filter.getText().addModifyListener(new ModifyListener() { - int modCount = 0; - //long lastModificationTime = -1000; - @Override - public void modifyText(ModifyEvent e) { - scheduleDelayedFilter(FILTER_DELAY, TimeUnit.MILLISECONDS); - } - private void scheduleDelayedFilter(long filterDelay, TimeUnit delayUnit) { - final String text = filter.getText().getText(); - - //long time = System.currentTimeMillis(); - //long delta = time - lastModificationTime; - //lastModificationTime = time; - - final int count = ++modCount; - ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() { - @Override - public void run() { - int newCount = modCount; - if (newCount != count) - return; - - ThreadUtils.asyncExec(swtThread, new Runnable() { - @Override - public void run() { - if (sc.isDisposed()) - return; - if (!filterGroups(text)) { - scheduleDelayedFilter(100, TimeUnit.MILLISECONDS); - } - } - }); - } - }, filterDelay, delayUnit); - } - }); - - sc = new ScrolledComposite(this, SWT.V_SCROLL); - GridDataFactory.fillDefaults().grab(true, true).applyTo(sc); - sc.setAlwaysShowScrollBars(false); - sc.setExpandHorizontal(false); - sc.setExpandVertical(false); - sc.getVerticalBar().setIncrement(30); - sc.getVerticalBar().setPageIncrement(200); - sc.addControlListener( new ControlAdapter() { - @Override - public void controlResized(ControlEvent e) { - //System.out.println("ScrolledComposite resized: " + sc.getSize()); - refreshScrolledComposite(); - } - }); - //sc.setBackground(sc.getDisplay().getSystemColor(SWT.COLOR_RED)); - - c = new Composite(sc, 0); - c.setVisible(false); - GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(c); - //c.setBackground(c.getDisplay().getSystemColor(SWT.COLOR_BLUE)); - - sc.setContent(c); - - // No event context <-> mouse on empty space in symbol library - SWTMouseEventAdapter noContextEventAdapter = new SWTMouseEventAdapter(null, externalEventHandler); - installMouseEventAdapter(sc, noContextEventAdapter); - installMouseEventAdapter(c, noContextEventAdapter); - - c.addDisposeListener(new DisposeListener() { - @Override - public void widgetDisposed(DisposeEvent e) { - // Remember to shutdown the executor - loaderExecutor.shutdown(); - groupViewers.clear(); - } - }); - } - - void refreshScrolledComposite() { - // Execute asynchronously to give the UI events triggering this method - // call time to run through before actually doing any resizing. - // Otherwise the result will lag behind reality when scrollbar - // visibility is toggled by the toolkit. - ThreadUtils.asyncExec(swtThread, new Runnable() { - @Override - public void run() { - if (sc.isDisposed()) - return; - syncRefreshScrolledComposite(); - } - }); - } - - void syncRefreshScrolledComposite() { - // Execute asynchronously to give the UI events triggering this method - // call time to run through before actually doing any resizing. - // Otherwise the result will lag behind reality when scrollbar - // visibility is toggled by the toolkit. - Rectangle r = sc.getClientArea(); - Point contentSize = c.computeSize(r.width, SWT.DEFAULT); - //System.out.println("[" + Thread.currentThread() + "] computed content size: " + contentSize + ", " + r); - c.setSize(contentSize); - } - - /** - * (Re-)Load symbol groups, refresh the content - */ - void load(Collection _libraries) { - if (_libraries == null) - _libraries = Collections.emptyList(); - final Collection libraries = _libraries; - if (loaderExecutor.isShutdown()) - return; - loaderExecutor.execute(new Runnable() { - @Override - public void run() { - // Increment loadCount to signal that a new load cycle is on the way. - Integer loadId = loadCount.incrementAndGet(); - try { - loaderSemaphore.acquire(); - beginPopulate(loadId); - } catch (InterruptedException e) { - ExceptionUtils.logError(e); - } catch (RuntimeException e) { - loaderSemaphore.release(); - ExceptionUtils.logAndShowError(e); - } catch (Error e) { - loaderSemaphore.release(); - ExceptionUtils.logAndShowError(e); - } - } - - void beginPopulate(Integer loadId) { - synchronized (groups) { - // Must use toArray since groups are removed within the loop - for (Iterator> it = groups.entrySet().iterator(); it.hasNext();) { - Map.Entry entry = it.next(); - if (!libraries.contains(entry.getKey())) { - PGroup group = entry.getValue(); - it.remove(); - groupViewers.remove(entry.getKey()); - if (group != null && !group.isDisposed()) - ThreadUtils.asyncExec(swtThread, disposer(group)); - } - } - Set groupDescs = new TreeSet(groupComparator); - for (ISymbolGroup lib : libraries) { - PGroup group = groups.get(lib); - //String label = group != null ? group.getText() : lib.getName(); - String label = lib.getName(); - String description = lib.getDescription(); - groupDescs.add(new GroupDescriptor(lib, label, description, group)); - } - - // Populate all the missing groups. - IFilter groupFilter = currentGroupFilter; - populateGroups( - loaderExecutor, - null, - groupDescs.iterator(), - groupFilter, - loadId, - new Runnable() { - @Override - public void run() { - loaderSemaphore.release(); - } - }); - } - } - }); - } - - void populateGroups( - final ExecutorService exec, - final Control lastGroup, - final Iterator iter, - final IFilter groupFilter, - final Integer loadId, - final Runnable loadComplete) - { - // Check whether to still continue this population or not. - int currentLoadId = loadCount.get(); - if (currentLoadId != loadId) { - loadComplete.run(); - return; - } - - if (!iter.hasNext()) { - ThreadUtils.asyncExec(swtThread, new Runnable() { - @Override - public void run() { - if (filter.isDisposed() || c.isDisposed()) - return; - //filter.focus(); - c.setVisible(true); - } - }); - loadComplete.run(); - return; - } - - final GroupDescriptor desc = iter.next(); - - ThreadUtils.asyncExec(swtThread, new Runnable() { - @Override - public void run() { - // Must make sure that loadComplete is invoked under error - // circumstances. - try { - populateGroup(); - } catch (RuntimeException e) { - loadComplete.run(); - ExceptionUtils.logAndShowError(e); - } catch (Error e) { - loadComplete.run(); - ExceptionUtils.logAndShowError(e); - } - } - - public void populateGroup() { - if (c.isDisposed()) { - loadComplete.run(); - return; - } - // $ SWT-begin - //System.out.println("populating: " + desc.label); - PGroup group = desc.group; - Runnable chainedCompletionCallback = loadComplete; - if (group == null || group.isDisposed()) { - - group = new PGroup(c, SWT.NONE); -// group.addListener(SWT.KeyUp, filterActivationListener); -// group.addListener(SWT.KeyDown, filterActivationListener); -// group.addListener(SWT.FocusIn, filterActivationListener); -// group.addListener(SWT.FocusOut, filterActivationListener); -// group.addListener(SWT.MouseDown, filterActivationListener); -// group.addListener(SWT.MouseUp, filterActivationListener); -// group.addListener(SWT.MouseDoubleClick, filterActivationListener); -// group.addListener(SWT.Arm, filterActivationListener); - if (lastGroup != null) { - group.moveBelow(lastGroup); - } else { - group.moveAbove(null); - } - - installMouseEventAdapter(group, new SWTMouseEventAdapter(group, externalEventHandler)); - - groups.put(desc.lib, group); - - Boolean shouldBeExpanded = expandedGroups.get(symbolGroupToKey(desc.lib)); - if (shouldBeExpanded == null) - shouldBeExpanded = defaultExpanded; - group.setData(KEY_USER_EXPANDED, shouldBeExpanded); - - group.setExpanded(shouldBeExpanded); - group.setFont(resourceManager.createFont(FontDescriptor.createFrom(group.getFont()).setStyle(SWT.NORMAL).increaseHeight(-1))); - GridDataFactory.fillDefaults().align(SWT.FILL, SWT.BEGINNING).grab(true, false).applyTo(group); - GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(group); - group.addExpandListener(groupExpandListener); - - // Track group content changes if possible. - if (desc.lib instanceof IModifiableSymbolGroup) { - IModifiableSymbolGroup mod = (IModifiableSymbolGroup) desc.lib; - mod.addListener(groupListener); - } - - if (shouldBeExpanded) { - //System.out.println("WAS EXPANDED(" + desc.label + ", " + symbolGroupToKey(desc.lib) + ", " + shouldBeExpanded + ")"); - PGroup expandedGroup = group; - chainedCompletionCallback = () -> { - // Chain callback to expand this group when the loading is otherwise completed. - ThreadUtils.asyncExec(swtThread, () -> setExpandedState(expandedGroup, true, true)); - loadComplete.run(); - }; - } - } - - group.setData(SymbolLibraryKeys.KEY_GROUP, desc.lib); - group.setText(desc.label); - group.setToolTipText(desc.description); - - // Hide the group immediately if necessary. - boolean groupFiltered = !groupFilter.select(desc.label); - group.setData(KEY_GROUP_FILTERED, Boolean.valueOf(groupFiltered)); - if (groupFiltered) - setGroupVisible(group, false); - - syncRefreshScrolledComposite(); - - final PGroup group_ = group; - Runnable newCompletionCallback = chainedCompletionCallback; - exec.execute(() -> { - populateGroups(exec, group_, iter, groupFilter, loadId, newCompletionCallback); - }); - } - }); - } - - protected void installMouseEventAdapter(Control onControl, SWTMouseEventAdapter eventAdapter) { - onControl.addMouseListener(eventAdapter); - onControl.addMouseTrackListener(eventAdapter); - onControl.addMouseMoveListener(eventAdapter); - onControl.addMouseWheelListener(eventAdapter); - } - - /** - * @param group - * @return null if GalleryViewer is currently being created - */ - GalleryViewer initializeGroup(final PGroup group) { - if (group.isDisposed()) - return null; - - //System.out.println("initializeGroup(" + group.getText() + ")"); - - synchronized (group) { - if (group.getData(KEY_VIEWER_INITIALIZED) != null) { - return (GalleryViewer) group.getData(SymbolLibraryKeys.KEY_GALLERY); - } - group.setData(KEY_VIEWER_INITIALIZED, Boolean.TRUE); - } - - //System.out.println("initializing group: " + group.getText()); - - // NOTE: this will NOT stop to wait until the SWT/AWT UI - // population has been completed. - GalleryViewer viewer = new GalleryViewer(group); - - ISymbolGroup input = (ISymbolGroup) group.getData(SymbolLibraryKeys.KEY_GROUP); - initializeViewer(group, input, viewer); - - groupViewers.put(input, viewer); - group.setData(SymbolLibraryKeys.KEY_GALLERY, viewer); - - //System.out.println("initialized group: " + group.getText()); - - return viewer; - } - - void initializeViewer(final PGroup group, final ISymbolGroup input, final GalleryViewer viewer) { - GridDataFactory.fillDefaults().align(SWT.FILL, SWT.BEGINNING).grab(true, false).applyTo(viewer.getControl()); - viewer.addDragSupport(new DragSourceParticipant()); - viewer.setAlign(FlowLayout.Align.Left); - viewer.getDiagram().setHint(SynchronizationHints.ERROR_HANDLER, errorHandler); - - viewer.setContentProvider(new IStructuredContentProvider() { - - /** - * Returns the elements in the input, which must be either an array or a - * Collection. - */ - @Override - public Object[] getElements(Object inputElement) { - if(inputElement == null) return new Object[0]; - return ((ISymbolGroup)inputElement).getItems(); - } - - /** - * This implementation does nothing. - */ - @Override - public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { - // do nothing. - } - - /** - * This implementation does nothing. - */ - @Override - public void dispose() { - // do nothing. - } - - }); - viewer.setLabelProvider(new LabelProvider()); - viewer.setInput(input); - - // Add event handler that closes libraries on double clicks into empty - // space in library. - viewer.getCanvasContext().getEventHandlerStack().add(new IEventHandler() { - @Override - public int getEventMask() { - return EventTypes.MouseDoubleClickMask; - } - - @Override - public boolean handleEvent(org.simantics.scenegraph.g2d.events.Event e) { - if (externalEventHandler.handleEvent(e)) - return true; - - if (e instanceof MouseDoubleClickedEvent) { - PickRequest req = new PickRequest(((MouseDoubleClickedEvent) e).controlPosition); - Collection result = new ArrayList(); - DiagramUtils.pick(viewer.getDiagram(), req, result); - if (!result.isEmpty()) - return false; - - //System.out.println("NOTHING CLICKED"); - if (group.isDisposed()) - return false; - group.getDisplay().asyncExec(() -> { - if (group.isDisposed()) - return; - - boolean exp = !group.getExpanded(); - group.setData(KEY_USER_EXPANDED, Boolean.valueOf(exp)); - setGroupExpandedWithoutNotification(group, exp); - refreshScrolledComposite(); - }); - return true; - } - return false; - } - }, 0); - } - - static String toPatternString(String filter) { - return DefaultFilterStrategy.defaultToPatternString(filter, true); - } - - static class SymbolItemFilter extends ViewerFilter { - private final String string; - private final Matcher m; - - public SymbolItemFilter(String string, Pattern pattern) { - this.string = string; - this.m = pattern.matcher(""); - } - - @Override - public boolean select(Viewer viewer, Object parentElement, Object element) { - if (element instanceof ISymbolItem) { - ISymbolItem item = (ISymbolItem) element; - return matchesFilter(item.getName()) || matchesFilter(item.getDescription()); - } else if (element instanceof ISymbolGroup) { - ISymbolGroup group = (ISymbolGroup) element; - return matchesFilter(group.getName()); - } - return false; - } - - private boolean matchesFilter(String str) { - m.reset(str.toLowerCase()); - boolean matches = m.matches(); - //System.out.println(pattern + ": " + str + ": " + (matches ? "PASS" : "FAIL")); - return matches; - } - - @Override - public int hashCode() { - return string == null ? 0 : string.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - SymbolItemFilter other = (SymbolItemFilter) obj; - if (string == null) { - if (other.string != null) - return false; - } else if (!string.equals(other.string)) - return false; - return true; - } - } - - static Pattern toPattern(String filterText) { - String regExFilter = toPatternString(filterText); - Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY; - return pattern; - } - - static IFilter composeFilter(final FilterConfiguration config) { - final Mode mode = config.getMode(); - final List patterns = new ArrayList(); - for (GroupFilter f : config.getFilters()) { - if (f.isActive()) - patterns.add(toPattern(f.getFilterText())); - } - return new IFilter() { - @Override - public boolean select(Object toTest) { - if (patterns.isEmpty()) - return true; - - String s = (String) toTest; - switch (mode) { - case AND: - for (Pattern pat : patterns) { - Matcher m = pat.matcher(s.toLowerCase()); - //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL")); - if (!m.matches()) - return false; - } - return true; - case OR: - for (Pattern pat : patterns) { - Matcher m = pat.matcher(s.toLowerCase()); - //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL")); - if (m.matches()) - return true; - } - return false; - default: - throw new Error("Shouldn't happen"); - } - } - }; - } - - void updateFilterConfiguration(FilterConfiguration config) { - this.config = config; - IFilter filter = composeFilter(config); - this.currentGroupFilter = filter; - } - - void applyGroupFilters() { - IFilter groupFilter = this.currentGroupFilter; - final boolean[] changed = new boolean[] { false }; - - Control[] grps = c.getChildren(); - for (Control ctrl : grps) { - final PGroup grp = (PGroup) ctrl; - boolean visible = grp.getVisible(); - boolean shouldBeVisible = groupFilter.select(grp.getText()); - boolean change = visible != shouldBeVisible; - changed[0] |= change; - - grp.setData(KEY_GROUP_FILTERED, Boolean.valueOf(!shouldBeVisible)); - if (change) { - setGroupVisible(grp, shouldBeVisible); - } - } - - ThreadUtils.asyncExec(swtThread, new Runnable() { - @Override - public void run() { - if (c.isDisposed()) - return; - if (changed[0]) { - c.layout(true); - syncRefreshScrolledComposite(); - } - } - }); - } - - /** - * Filters the symbol groups and makes them visible/invisible as necessary. - * Invoke only from the SWT thread. - * - * @param text the filter text given by the client - * @return true if all groups were successfully filtered - * without asynchronous results - */ - boolean filterGroups(String text) { - //System.out.println("FILTERING WITH TEXT: " + text); - - String regExFilter = toPatternString(text); - Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY; - - this.currentFilterPattern = pattern; - final boolean[] changed = new boolean[] { false }; - boolean filteringComplete = true; - - ViewerFilter filter = null; - if (regExFilter != null) - filter = new SymbolItemFilter(regExFilter, pattern); - - Control[] grps = c.getChildren(); - for (Control ctrl : grps) { - final PGroup grp = (PGroup) ctrl; - if (grp.isDisposed()) - continue; - Boolean contentsChanged = filterGroup(grp, filter); - if (contentsChanged == null) - filteringComplete = false; - else - changed[0] = contentsChanged; - } - - ThreadUtils.asyncExec(swtThread, new Runnable() { - @Override - public void run() { - if (c.isDisposed()) - return; - if (changed[0]) { - c.layout(true); - syncRefreshScrolledComposite(); - } - } - }); - - return filteringComplete; - } - - static boolean objectEquals(Object o1, Object o2) { - if (o1==o2) return true; - if (o1==null && o2==null) return true; - if (o1==null || o2==null) return false; - return o1.equals(o2); - } - - /** - * @param grp - * @return true if the filtering caused changes in the group, - * false if not, and null if filtering - * could not be performed yet, meaning results need to be asked - * later - */ - private Boolean filterGroup(PGroup grp, ViewerFilter filter) { - boolean changed = false; - GalleryViewer viewer = initializeGroup(grp); - if (viewer == null) - return null; - - ViewerFilter lastFilter = viewer.getFilter(); - - boolean groupFiltered = Boolean.TRUE.equals(grp.getData(KEY_GROUP_FILTERED)); - boolean userExpanded = Boolean.TRUE.equals(grp.getData(KEY_USER_EXPANDED)); - final boolean expanded = grp.getExpanded(); - final boolean visible = grp.getVisible(); - final boolean filterChanged = !objectEquals(filter, lastFilter); - - // Find out how much data would be shown with the new filter. - viewer.setFilter(filter); - Object[] elements = viewer.getFilteredElements(); - - ISymbolGroup symbolGroup = (ISymbolGroup) grp.getData(SymbolLibraryKeys.KEY_GROUP); - boolean filterMatchesGroup = filter != null && filter.select(viewer, null, symbolGroup); - boolean shouldBeVisible = !groupFiltered && (elements.length > 0 || filterMatchesGroup); - boolean shouldBeExpanded = shouldBeVisible && (filter != null || userExpanded); - -// System.out.format("%40s: visible/should be = %5s %5s, expanded/user expanded/should be = %5s %5s %5s\n", -// grp.getText(), -// String.valueOf(visible), -// String.valueOf(shouldBeVisible), -// String.valueOf(expanded), -// String.valueOf(userExpanded), -// String.valueOf(shouldBeExpanded)); - - if (filterChanged || visible != shouldBeVisible || expanded != shouldBeExpanded) { - changed = true; - - if (shouldBeVisible == userExpanded) { - if (expanded != shouldBeExpanded) - setGroupExpandedWithoutNotification(grp, shouldBeExpanded); - setGroupVisible(grp, shouldBeVisible); - } else { - if (filter != null) { - if (shouldBeVisible) { - // The user has not expanded this group but the group contains - // stuff that matches the non-empty filter => show the group. - setGroupExpandedWithoutNotification(grp, true); - setGroupVisible(grp, true); - } else { - // The user has expanded this group but it does not contain items - // should should be shown with the current non-empty filter => hide the group. - setGroupExpandedWithoutNotification(grp, true); - setGroupVisible(grp, false); - } - } else { - // All groups should be visible. Some should be expanded and others not. - if (expanded != userExpanded) - setGroupExpandedWithoutNotification(grp, userExpanded); - if (!visible) - setGroupVisible(grp, true); - } - } - - if (shouldBeExpanded) { - viewer.refreshWithContent(elements); - } - } - -// String label = grp.getText(); -// Matcher m = pattern.matcher(label.toLowerCase()); -// boolean visible = m.matches(); -// if (visible != grp.getVisible()) { -// changed = true; -// setGroupVisible(grp, visible); -// } - - return changed; - } - - void setGroupExpandedWithoutNotification(PGroup grp, boolean expanded) { - // Ok, don't need to remove/add expand listener, PGroup will not notify - // listeners when setExpanded is invoked. - //grp.removeExpandListener(groupExpandListener); - storeGroupExpandedState(grp, expanded); - grp.setExpanded(expanded); - //grp.addExpandListener(groupExpandListener); - } - - void setGroupVisible(PGroup group, boolean visible) { - GridData gd = (GridData) group.getLayoutData(); - gd.exclude = !visible; - group.setVisible(visible); - } - - boolean isGroupFiltered(String label) { - return !currentFilterPattern.matcher(label.toLowerCase()).matches(); - } - - class DragSourceParticipant extends AbstractDiagramParticipant implements IDragSourceParticipant { - @Reference Selection selection; - @Dependency PointerInteractor pi; - @Dependency TransformUtil util; - @Dependency PickContext pickContext; - - @Override - public int canDrag(MouseDragBegin me) { - if (me.button != MouseEvent.LEFT_BUTTON) return 0; - if (getHint(Hints.KEY_TOOL) != Hints.POINTERTOOL) return 0; - assertDependencies(); - - PickRequest req = new PickRequest(me.startCanvasPos); - req.pickPolicy = PickRequest.PickPolicy.PICK_INTERSECTING_OBJECTS; - List picks = new ArrayList(); - pickContext.pick(diagram, req, picks); - Set sel = selection.getSelection(me.mouseId); - - if (Collections.disjoint(sel, picks)) return 0; - // Box Select - return DnDConstants.ACTION_COPY; - } - - @Override - public Transferable dragStart(DragGestureEvent e) { - - AWTChassis chassis = (AWTChassis) e.getComponent(); - ICanvasContext cc = chassis.getCanvasContext(); - Selection sel = cc.getSingleItem(Selection.class); - - Set ss = sel.getSelection(0); - if (ss.isEmpty()) return null; - Object[] res = new Object[ss.size()]; - int index = 0; - for (IElement ee : ss) - res[index++] = ee.getHint(ElementHints.KEY_OBJECT); - - ISelection object = new StructuredSelection(res); - - LocalObjectTransferable local = new LocalObjectTransferable(object); - - StringBuilder json = new StringBuilder(); - json.append("{"); - json.append(" \"type\" : \"Symbol\","); - json.append(" \"res\" : ["); - int pos = 0; - for(int i=0;i 0) json.append(","); - Object r = res[i]; - if(r instanceof IdentifiedObject) { - Object id = ((IdentifiedObject) r).getId(); - if(id instanceof IAdaptable) { - Object resource = ((IAdaptable) id).getAdapter(Resource.class); - if(resource != null) { - long rid = ((Resource)resource).getResourceId(); - json.append(Long.toString(rid)); - pos++; - } - } - } - } - json.append("] }"); - - StringSelection text = new StringSelection(json.toString()); - PlaintextTransfer plainText = new PlaintextTransfer(json.toString()); - - return new MultiTransferable(local, text, plainText); - - } - - @Override - public int getAllowedOps() { - return DnDConstants.ACTION_COPY; - } - @Override - public void dragDropEnd(DragSourceDropEvent dsde) { -// System.out.println("dragDropEnd: " + dsde); - LocalObjectTransfer.getTransfer().clear(); - } - @Override - public void dragEnter(DragSourceDragEvent dsde) { - } - @Override - public void dragExit(DragSourceEvent dse) { - } - @Override - public void dragOver(DragSourceDragEvent dsde) { - } - @Override - public void dropActionChanged(DragSourceDragEvent dsde) { - } - } - - ExpandListener groupExpandListener = new ExpandListener() { - @Override - public void itemCollapsed(ExpandEvent e) { - final PGroup group = (PGroup) e.widget; - group.setData(KEY_USER_EXPANDED, Boolean.FALSE); - storeGroupExpandedState(group, false); - //System.out.println("item collapsed: " + group + ", " + sc.getClientArea()); - refreshScrolledComposite(); - } - @Override - public void itemExpanded(ExpandEvent e) { - final PGroup group = (PGroup) e.widget; - group.setData(KEY_USER_EXPANDED, Boolean.TRUE); - storeGroupExpandedState(group, true); - //System.out.println("item expanded: " + group + ", " + sc.getClientArea()); - ThreadUtils.asyncExec(swtThread, () -> { - GalleryViewer viewer = initializeGroup(group); - if (viewer == null) - return; - ThreadUtils.asyncExec(swtThread, () -> { - if (viewer.getControl().isDisposed()) - return; - viewer.refresh(); - refreshScrolledComposite(); - }); - }); - } - }; - - public boolean isDefaultExpanded() { - return defaultExpanded; - } - - public void setDefaultExpanded(boolean defaultExpanded) { - this.defaultExpanded = defaultExpanded; - } - - Runnable disposer(final Widget w) { - return new Runnable() { - @Override - public void run() { - if (w.isDisposed()) - return; - w.dispose(); - } - }; - } - - /** - * Invoke from SWT thread only. - * - * @param targetState - */ - public void setAllExpandedStates(boolean targetState) { - setDefaultExpanded(targetState); - Control[] grps = c.getChildren(); - boolean changed = false; - for (Control control : grps) - changed |= setExpandedState((PGroup) control, targetState, false); - if (changed) - refreshScrolledComposite(); - } - - /** - * Invoke from SWT thread only. - * - * @param grp - * @param targetState - * @return - */ - boolean setExpandedState(PGroup grp, boolean targetState, boolean force) { - if (grp.isDisposed()) - return false; - - storeGroupExpandedState(grp, targetState); - grp.setData(KEY_USER_EXPANDED, Boolean.valueOf(targetState)); - if ((force || grp.getExpanded() != targetState) && grp.getVisible()) { - final GalleryViewer viewer = initializeGroup(grp); - setGroupExpandedWithoutNotification(grp, targetState); - ThreadUtils.asyncExec(swtThread, () -> { - if (!grp.isDisposed()) { - if (viewer != null) - viewer.refresh(); - refreshScrolledComposite(); - } - }); - return true; - } - return false; - } - - class ImageLoader implements Runnable { - - private final ImageProxy imageProxy; - private final ISymbolItem item; - - public ImageLoader(ImageProxy imageProxy, ISymbolItem item) { - this.imageProxy = imageProxy; - this.item = item; - } - - @Override - public void run() { - // SVG images using the SVGUniverse in SVGCache must use - // AWT thread for all operations. - ThreadUtils.asyncExec(AWTThread.getThreadAccess(), () -> runBlocking()); - } - - private void runBlocking() { - try { - ISymbolGroup group = item.getGroup(); - if (group == null) - throw new ProvisionException("No ISymbolGroup available for ISymbolItem " + item); - - GalleryViewer viewer = groupViewers.get(group); - if (viewer == null) { - // This is normal if this composite has been disposed while these are being ran. - //throw new ProvisionException("No GalleryViewer available ISymbolGroup " + group); - imageProxy.setSource(DefaultImages.UNKNOWN2.get()); - return; - } - - IHintContext hints = viewer.getDiagram(); - if (hints == null) - throw new ProvisionException("No diagram available for GalleryViewer of group " + group); - - hints.setHint(ISymbolItem.KEY_ELEMENT_CLASS_LISTENER, new ElementClassListener(imageCache, disposed, item)); - final ElementClass ec = item.getElementClass(hints); - - // Without this the symbol library will at times - // not update the final graphics for the symbol. - // It will keep displaying the hourglass pending icon instead. - symbolUpdate(disposed, imageProxy, ec); - } catch (ProvisionException e) { - ExceptionUtils.logWarning("Failed to provide element class for symbol item " + item, e); - imageProxy.setSource(DefaultImages.ERROR_DECORATOR.get()); - } catch (Exception e) { - ExceptionUtils.logError(e); - imageProxy.setSource(DefaultImages.ERROR_DECORATOR.get()); - } finally { - } - } - } - - static class ElementClassListener implements org.simantics.db.procedure.Listener { - private Map> imageCache; - private final AtomicBoolean disposed; - private final ISymbolItem item; - - public ElementClassListener(Map> imageCache, AtomicBoolean disposed, ISymbolItem item) { - this.imageCache = imageCache; - this.disposed = disposed; - this.item = item; - } - - @Override - public void execute(final ElementClass ec) { - //System.out.println("SYMBOL CHANGED: " + item + " - disposed=" + disposed + " - " + ec); - - final ImageProxy[] imageProxy = { null }; - SoftReference proxyRef = imageCache.get(item); - if (proxyRef != null) - imageProxy[0] = proxyRef.get(); - if (imageProxy[0] != null) - scheduleSymbolUpdate(disposed, imageProxy[0], ec); - } - - @Override - public void exception(Throwable t) { - Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error in ElementClass request.", t)); - } - - @Override - public boolean isDisposed() { - //System.out.println("ElementClassListener.isDisposed " + item + " - " + disposed.get()); - return disposed.get(); - } - } - - public FilterArea getFilterArea() { - return filter; - } - - public static void scheduleSymbolUpdate(final AtomicBoolean disposed, final ImageProxy imageProxy, final ElementClass ec) { - if (disposed.get()) - return; - ThreadUtils.asyncExec(AWTThread.getThreadAccess(), new Runnable() { - @Override - public void run() { - if (disposed.get()) - return; - symbolUpdate(disposed, imageProxy, ec); - } - }); - } - - public static void symbolUpdate(final AtomicBoolean disposed, final ImageProxy imageProxy, final ElementClass ec) { - StaticSymbol ss = ec.getSingleItem(StaticSymbol.class); - Image source = ss == null ? DefaultImages.UNKNOWN2.get() : ss.getImage(); - imageProxy.setSource(source); - } - - Runnable filterActivator = new Runnable() { - @Override - public void run() { - filter.focus(); - } - }; - Listener filterActivationListener = new Listener() { - @Override - public void handleEvent(Event event) { - //System.out.println("event: " + event); - filterActivator.run(); - } - }; - - ISymbolGroupListener groupListener = new ISymbolGroupListener() { - @Override - public void itemsChanged(ISymbolGroup group) { - //System.out.println("symbol group changed: " + group); - GalleryViewer viewer = groupViewers.get(group); - if (viewer != null) { - ISymbolItem[] input = group.getItems(); - viewer.setInput(input); - } - } - }; - - IEventHandler externalEventHandler = new IEventHandler() { - @Override - public int getEventMask() { - return EventTypes.AnyMask; - } - @Override - public boolean handleEvent(org.simantics.scenegraph.g2d.events.Event e) { - IEventHandler handler = SymbolLibraryComposite.this.eventHandler; - return handler != null && EventTypes.passes(handler, e) ? handler.handleEvent(e) : false; - } - }; - - protected volatile IEventHandler eventHandler; - - /** - * @param eventHandler - */ - public void setEventHandler(IEventHandler eventHandler) { - this.eventHandler = eventHandler; - } - - protected void storeGroupExpandedState(PGroup group, boolean expanded) { - ISymbolGroup symbolGroup = (ISymbolGroup) group.getData(SymbolLibraryKeys.KEY_GROUP); - //System.out.println("setGroupExpandedWithoutNotification(" + group + ", " + expanded + ", " + symbolGroup + ")"); - if (symbolGroup != null) { - Object key = symbolGroupToKey(symbolGroup); - expandedGroups.put(key, expanded ? Boolean.TRUE : Boolean.FALSE); - } - } - - private static Object symbolGroupToKey(ISymbolGroup symbolGroup) { - if (symbolGroup instanceof IIdentifiedObject) - return ((IIdentifiedObject) symbolGroup).getId(); - return new Tuple2(symbolGroup.getName(), symbolGroup.getDescription()); - } - -} +/******************************************************************************* + * 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.diagram.symbollibrary.ui; + +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DragGestureEvent; +import java.awt.dnd.DragSourceDragEvent; +import java.awt.dnd.DragSourceDropEvent; +import java.awt.dnd.DragSourceEvent; +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.WeakHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Semaphore; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.resource.FontDescriptor; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.resource.LocalResourceManager; +import org.eclipse.jface.viewers.AcceptAllFilter; +import org.eclipse.jface.viewers.BaseLabelProvider; +import org.eclipse.jface.viewers.IFilter; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerFilter; +import org.eclipse.nebula.widgets.pgroup.PGroup; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.ExpandEvent; +import org.eclipse.swt.events.ExpandListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Widget; +import org.simantics.Simantics; +import org.simantics.db.ReadGraph; +import org.simantics.db.Resource; +import org.simantics.db.common.procedure.adapter.ListenerAdapter; +import org.simantics.db.common.request.UnaryRead; +import org.simantics.db.exception.DatabaseException; +import org.simantics.diagram.internal.Activator; +import org.simantics.diagram.symbolcontribution.CompositeSymbolGroup; +import org.simantics.diagram.symbolcontribution.IIdentifiedObject; +import org.simantics.diagram.symbolcontribution.ISymbolProvider; +import org.simantics.diagram.symbolcontribution.SymbolProviderFactory; +import org.simantics.diagram.symbollibrary.IModifiableSymbolGroup; +import org.simantics.diagram.symbollibrary.ISymbolGroup; +import org.simantics.diagram.symbollibrary.ISymbolGroupListener; +import org.simantics.diagram.symbollibrary.ISymbolItem; +import org.simantics.diagram.symbollibrary.ui.FilterConfiguration.Mode; +import org.simantics.diagram.synchronization.ErrorHandler; +import org.simantics.diagram.synchronization.LogErrorHandler; +import org.simantics.diagram.synchronization.SynchronizationHints; +import org.simantics.g2d.canvas.Hints; +import org.simantics.g2d.canvas.ICanvasContext; +import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency; +import org.simantics.g2d.canvas.impl.DependencyReflection.Reference; +import org.simantics.g2d.chassis.AWTChassis; +import org.simantics.g2d.diagram.DiagramUtils; +import org.simantics.g2d.diagram.handler.PickContext; +import org.simantics.g2d.diagram.handler.PickRequest; +import org.simantics.g2d.diagram.handler.layout.FlowLayout; +import org.simantics.g2d.diagram.participant.AbstractDiagramParticipant; +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.element.ElementClass; +import org.simantics.g2d.element.ElementHints; +import org.simantics.g2d.element.IElement; +import org.simantics.g2d.element.handler.StaticSymbol; +import org.simantics.g2d.event.adapter.SWTMouseEventAdapter; +import org.simantics.g2d.gallery.GalleryViewer; +import org.simantics.g2d.gallery.ILabelProvider; +import org.simantics.g2d.image.DefaultImages; +import org.simantics.g2d.image.Image; +import org.simantics.g2d.image.Image.Feature; +import org.simantics.g2d.image.impl.ImageProxy; +import org.simantics.g2d.participant.TransformUtil; +import org.simantics.scenegraph.g2d.events.EventTypes; +import org.simantics.scenegraph.g2d.events.IEventHandler; +import org.simantics.scenegraph.g2d.events.MouseEvent; +import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent; +import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin; +import org.simantics.scl.runtime.tuple.Tuple2; +import org.simantics.ui.dnd.LocalObjectTransfer; +import org.simantics.ui.dnd.LocalObjectTransferable; +import org.simantics.ui.dnd.MultiTransferable; +import org.simantics.ui.dnd.PlaintextTransfer; +import org.simantics.utils.datastructures.cache.ProvisionException; +import org.simantics.utils.datastructures.hints.IHintContext; +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.ui.ErrorLogger; +import org.simantics.utils.ui.ExceptionUtils; + +/** + * @author Tuukka Lehtonen + */ +public class SymbolLibraryComposite extends Composite { + + private static final int FILTER_DELAY = 500; + + private static final String KEY_VIEWER_INITIALIZED = "viewer.initialized"; + private static final String KEY_USER_EXPANDED = "userExpanded"; + private static final String KEY_GROUP_FILTERED = "groupFiltered"; + + /** Root composite */ + ScrolledComposite sc; + Composite c; + IThreadWorkQueue swtThread; + boolean defaultExpanded = false; + ISymbolProvider symbolProvider; + AtomicBoolean disposed = new AtomicBoolean(false); + + /** + * This value is incremented each time a load method is called and symbol + * group population is started. It can be used by + * {@link #populateGroups(ExecutorService, Control, Iterator, IFilter)} to + * tell whether it should stop its population job because a later load + * will override its results anyway. + */ + AtomicInteger loadCount = new AtomicInteger(); + + Map groups = new HashMap<>(); + Map groupViewers = new HashMap<>(); + Map expandedGroups = new HashMap<>(); + LocalResourceManager resourceManager; + FilterArea filter; + + ThreadFactory threadFactory = new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "Symbol Library Loader"); + t.setDaemon(false); + t.setPriority(Thread.NORM_PRIORITY); + return t; + } + }; + + Semaphore loaderSemaphore = new Semaphore(1); + ExecutorService loaderExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 2L, TimeUnit.SECONDS, + new SynchronousQueue(), + threadFactory); + + /** + * Used to prevent annoying reloading of symbols when groups are closed and + * reopened by not always having to schedule an {@link ImageLoader} in + * {@link LabelProvider#getImage(Object)}. + */ + Map> imageCache = new WeakHashMap>(); + + static final Pattern ANY = Pattern.compile(".*"); + Pattern currentFilterPattern = ANY; + + FilterConfiguration config = new FilterConfiguration(); + IFilter currentGroupFilter = AcceptAllFilter.getInstance(); + + ErrorHandler errorHandler = LogErrorHandler.INSTANCE; + + static class GroupDescriptor { + public final ISymbolGroup lib; + public final String label; + public final String description; + public final PGroup group; + + public GroupDescriptor(ISymbolGroup lib, String label, String description, PGroup group) { + assert(lib != null); + assert(label != null); + this.lib = lib; + this.label = label; + this.description = description; + this.group = group; + } + } + + Comparator groupComparator = new Comparator() { + @Override + public int compare(GroupDescriptor o1, GroupDescriptor o2) { + return o1.label.compareToIgnoreCase(o2.label); + } + }; + + static final EnumSet VOLATILE = EnumSet.of(Feature.Volatile); + + static class PendingImage extends ImageProxy { + EnumSet features; + PendingImage(Image source, EnumSet features) { + super(source); + this.features = features; + } + @Override + public EnumSet getFeatures() { + return features; + } + } + + class LabelProvider extends BaseLabelProvider implements ILabelProvider { + @Override + public Image getImage(final Object element) { + ISymbolItem item = (ISymbolItem) element; + // Use a volatile ImageProxy to make the image loading asynchronous. + ImageProxy proxy = null; + SoftReference proxyRef = imageCache.get(item); + if (proxyRef != null) + proxy = proxyRef.get(); + if (proxy == null) { + proxy = new PendingImage(DefaultImages.HOURGLASS.get(), VOLATILE); + imageCache.put(item, new SoftReference(proxy)); + ThreadUtils.getNonBlockingWorkExecutor().schedule(new ImageLoader(proxy, item), 100, TimeUnit.MILLISECONDS); + } + return proxy; + } + @Override + public String getText(final Object element) { + return ((ISymbolItem) element).getName(); + } + @Override + public String getToolTipText(Object element) { + ISymbolItem item = (ISymbolItem) element; + String name = item.getName(); + String desc = item.getDescription(); + return name.equals(desc) ? name : name + " - " + desc; + } + + @Override + public java.awt.Image getToolTipImage(Object object) { + return null; + } + @Override + public Color getToolTipBackgroundColor(Object object) { + return null; + } + + @Override + public Color getToolTipForegroundColor(Object object) { + return null; + } + } + + public SymbolLibraryComposite(final Composite parent, int style, SymbolProviderFactory symbolProvider) { + super(parent, style); + init(parent, style); + Simantics.getSession().asyncRequest(new CreateSymbolProvider(symbolProvider), new SymbolProviderListener()); + addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + disposed.set(true); + } + }); + } + + /** + * + */ + static class CreateSymbolProvider extends UnaryRead { + public CreateSymbolProvider(SymbolProviderFactory factory) { + super(factory); + } + @Override + public ISymbolProvider perform(ReadGraph graph) throws DatabaseException { + //System.out.println("CreateSymbolProvider.perform: " + parameter); + ISymbolProvider provider = parameter.create(graph); + //print(provider); + return provider; + } + } + + @SuppressWarnings("unused") + private static void print(ISymbolProvider provider) { + for (ISymbolGroup grp : provider.getSymbolGroups()) { + System.out.println("GROUP: " + grp); + if (grp instanceof CompositeSymbolGroup) { + CompositeSymbolGroup cgrp = (CompositeSymbolGroup) grp; + for (ISymbolGroup grp2 : cgrp.getGroups()) { + System.out.println("\tGROUP: " + grp2); + } + } + } + } + + /** + * + */ + class SymbolProviderListener extends ListenerAdapter { + @Override + public void exception(Throwable t) { + ErrorLogger.defaultLogError(t); + } + @Override + public void execute(ISymbolProvider result) { + //System.out.println("SymbolProviderListener: " + result); + symbolProvider = result; + if (result != null) { + Collection groups = result.getSymbolGroups(); + //print(result); + load(groups); + } + } + public boolean isDisposed() { + boolean result = SymbolLibraryComposite.this.isDisposed(); + return result; + } + } + + private void init(final Composite parent, int style) { + GridLayoutFactory.fillDefaults().spacing(0,0).applyTo(this); +// setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_RED)); + + this.resourceManager = new LocalResourceManager(JFaceResources.getResources(getDisplay()), this); + swtThread = SWTThread.getThreadAccess(this); + + filter = new FilterArea(this, SWT.NONE); + GridDataFactory.fillDefaults().grab(true, false).applyTo(filter); + filter.getText().addModifyListener(new ModifyListener() { + int modCount = 0; + //long lastModificationTime = -1000; + @Override + public void modifyText(ModifyEvent e) { + scheduleDelayedFilter(FILTER_DELAY, TimeUnit.MILLISECONDS); + } + private void scheduleDelayedFilter(long filterDelay, TimeUnit delayUnit) { + final String text = filter.getText().getText(); + + //long time = System.currentTimeMillis(); + //long delta = time - lastModificationTime; + //lastModificationTime = time; + + final int count = ++modCount; + ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() { + @Override + public void run() { + int newCount = modCount; + if (newCount != count) + return; + + ThreadUtils.asyncExec(swtThread, new Runnable() { + @Override + public void run() { + if (sc.isDisposed()) + return; + if (!filterGroups(text)) { + scheduleDelayedFilter(100, TimeUnit.MILLISECONDS); + } + } + }); + } + }, filterDelay, delayUnit); + } + }); + + sc = new ScrolledComposite(this, SWT.V_SCROLL); + GridDataFactory.fillDefaults().grab(true, true).applyTo(sc); + sc.setAlwaysShowScrollBars(false); + sc.setExpandHorizontal(false); + sc.setExpandVertical(false); + sc.getVerticalBar().setIncrement(30); + sc.getVerticalBar().setPageIncrement(200); + sc.addControlListener( new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + //System.out.println("ScrolledComposite resized: " + sc.getSize()); + refreshScrolledComposite(); + } + }); + //sc.setBackground(sc.getDisplay().getSystemColor(SWT.COLOR_RED)); + + c = new Composite(sc, 0); + c.setVisible(false); + GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(c); + //c.setBackground(c.getDisplay().getSystemColor(SWT.COLOR_BLUE)); + + sc.setContent(c); + + // No event context <-> mouse on empty space in symbol library + SWTMouseEventAdapter noContextEventAdapter = new SWTMouseEventAdapter(null, externalEventHandler); + installMouseEventAdapter(sc, noContextEventAdapter); + installMouseEventAdapter(c, noContextEventAdapter); + + c.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + // Remember to shutdown the executor + loaderExecutor.shutdown(); + groupViewers.clear(); + } + }); + } + + void refreshScrolledComposite() { + // Execute asynchronously to give the UI events triggering this method + // call time to run through before actually doing any resizing. + // Otherwise the result will lag behind reality when scrollbar + // visibility is toggled by the toolkit. + ThreadUtils.asyncExec(swtThread, new Runnable() { + @Override + public void run() { + if (sc.isDisposed()) + return; + syncRefreshScrolledComposite(); + } + }); + } + + void syncRefreshScrolledComposite() { + // Execute asynchronously to give the UI events triggering this method + // call time to run through before actually doing any resizing. + // Otherwise the result will lag behind reality when scrollbar + // visibility is toggled by the toolkit. + Rectangle r = sc.getClientArea(); + Point contentSize = c.computeSize(r.width, SWT.DEFAULT); + //System.out.println("[" + Thread.currentThread() + "] computed content size: " + contentSize + ", " + r); + c.setSize(contentSize); + } + + /** + * (Re-)Load symbol groups, refresh the content + */ + void load(Collection _libraries) { + if (_libraries == null) + _libraries = Collections.emptyList(); + final Collection libraries = _libraries; + if (loaderExecutor.isShutdown()) + return; + loaderExecutor.execute(new Runnable() { + @Override + public void run() { + // Increment loadCount to signal that a new load cycle is on the way. + Integer loadId = loadCount.incrementAndGet(); + try { + loaderSemaphore.acquire(); + beginPopulate(loadId); + } catch (InterruptedException e) { + ExceptionUtils.logError(e); + } catch (RuntimeException e) { + loaderSemaphore.release(); + ExceptionUtils.logAndShowError(e); + } catch (Error e) { + loaderSemaphore.release(); + ExceptionUtils.logAndShowError(e); + } + } + + void beginPopulate(Integer loadId) { + synchronized (groups) { + // Must use toArray since groups are removed within the loop + for (Iterator> it = groups.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = it.next(); + if (!libraries.contains(entry.getKey())) { + PGroup group = entry.getValue(); + it.remove(); + groupViewers.remove(entry.getKey()); + if (group != null && !group.isDisposed()) + ThreadUtils.asyncExec(swtThread, disposer(group)); + } + } + Set groupDescs = new TreeSet(groupComparator); + for (ISymbolGroup lib : libraries) { + PGroup group = groups.get(lib); + //String label = group != null ? group.getText() : lib.getName(); + String label = lib.getName(); + String description = lib.getDescription(); + groupDescs.add(new GroupDescriptor(lib, label, description, group)); + } + + // Populate all the missing groups. + IFilter groupFilter = currentGroupFilter; + populateGroups( + loaderExecutor, + null, + groupDescs.iterator(), + groupFilter, + loadId, + new Runnable() { + @Override + public void run() { + loaderSemaphore.release(); + } + }); + } + } + }); + } + + void populateGroups( + final ExecutorService exec, + final Control lastGroup, + final Iterator iter, + final IFilter groupFilter, + final Integer loadId, + final Runnable loadComplete) + { + // Check whether to still continue this population or not. + int currentLoadId = loadCount.get(); + if (currentLoadId != loadId) { + loadComplete.run(); + return; + } + + if (!iter.hasNext()) { + ThreadUtils.asyncExec(swtThread, new Runnable() { + @Override + public void run() { + if (filter.isDisposed() || c.isDisposed()) + return; + //filter.focus(); + c.setVisible(true); + } + }); + loadComplete.run(); + return; + } + + final GroupDescriptor desc = iter.next(); + + ThreadUtils.asyncExec(swtThread, new Runnable() { + @Override + public void run() { + // Must make sure that loadComplete is invoked under error + // circumstances. + try { + populateGroup(); + } catch (RuntimeException e) { + loadComplete.run(); + ExceptionUtils.logAndShowError(e); + } catch (Error e) { + loadComplete.run(); + ExceptionUtils.logAndShowError(e); + } + } + + public void populateGroup() { + if (c.isDisposed()) { + loadComplete.run(); + return; + } + // $ SWT-begin + //System.out.println("populating: " + desc.label); + PGroup group = desc.group; + Runnable chainedCompletionCallback = loadComplete; + if (group == null || group.isDisposed()) { + + group = new PGroup(c, SWT.NONE); +// group.addListener(SWT.KeyUp, filterActivationListener); +// group.addListener(SWT.KeyDown, filterActivationListener); +// group.addListener(SWT.FocusIn, filterActivationListener); +// group.addListener(SWT.FocusOut, filterActivationListener); +// group.addListener(SWT.MouseDown, filterActivationListener); +// group.addListener(SWT.MouseUp, filterActivationListener); +// group.addListener(SWT.MouseDoubleClick, filterActivationListener); +// group.addListener(SWT.Arm, filterActivationListener); + if (lastGroup != null) { + group.moveBelow(lastGroup); + } else { + group.moveAbove(null); + } + + installMouseEventAdapter(group, new SWTMouseEventAdapter(group, externalEventHandler)); + + groups.put(desc.lib, group); + + Boolean shouldBeExpanded = expandedGroups.get(symbolGroupToKey(desc.lib)); + if (shouldBeExpanded == null) + shouldBeExpanded = defaultExpanded; + group.setData(KEY_USER_EXPANDED, shouldBeExpanded); + + group.setExpanded(shouldBeExpanded); + group.setFont(resourceManager.createFont(FontDescriptor.createFrom(group.getFont()).setStyle(SWT.NORMAL).increaseHeight(-1))); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.BEGINNING).grab(true, false).applyTo(group); + GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(group); + group.addExpandListener(groupExpandListener); + + // Track group content changes if possible. + if (desc.lib instanceof IModifiableSymbolGroup) { + IModifiableSymbolGroup mod = (IModifiableSymbolGroup) desc.lib; + mod.addListener(groupListener); + } + + if (shouldBeExpanded) { + //System.out.println("WAS EXPANDED(" + desc.label + ", " + symbolGroupToKey(desc.lib) + ", " + shouldBeExpanded + ")"); + PGroup expandedGroup = group; + chainedCompletionCallback = () -> { + // Chain callback to expand this group when the loading is otherwise completed. + ThreadUtils.asyncExec(swtThread, () -> setExpandedState(expandedGroup, true, true)); + loadComplete.run(); + }; + } + } + + group.setData(SymbolLibraryKeys.KEY_GROUP, desc.lib); + group.setText(desc.label); + group.setToolTipText(desc.description); + + // Hide the group immediately if necessary. + boolean groupFiltered = !groupFilter.select(desc.label); + group.setData(KEY_GROUP_FILTERED, Boolean.valueOf(groupFiltered)); + if (groupFiltered) + setGroupVisible(group, false); + + syncRefreshScrolledComposite(); + + final PGroup group_ = group; + Runnable newCompletionCallback = chainedCompletionCallback; + exec.execute(() -> { + populateGroups(exec, group_, iter, groupFilter, loadId, newCompletionCallback); + }); + } + }); + } + + protected void installMouseEventAdapter(Control onControl, SWTMouseEventAdapter eventAdapter) { + onControl.addMouseListener(eventAdapter); + onControl.addMouseTrackListener(eventAdapter); + onControl.addMouseMoveListener(eventAdapter); + onControl.addMouseWheelListener(eventAdapter); + } + + /** + * @param group + * @return null if GalleryViewer is currently being created + */ + GalleryViewer initializeGroup(final PGroup group) { + if (group.isDisposed()) + return null; + + //System.out.println("initializeGroup(" + group.getText() + ")"); + + synchronized (group) { + if (group.getData(KEY_VIEWER_INITIALIZED) != null) { + return (GalleryViewer) group.getData(SymbolLibraryKeys.KEY_GALLERY); + } + group.setData(KEY_VIEWER_INITIALIZED, Boolean.TRUE); + } + + //System.out.println("initializing group: " + group.getText()); + + // NOTE: this will NOT stop to wait until the SWT/AWT UI + // population has been completed. + GalleryViewer viewer = new GalleryViewer(group); + + ISymbolGroup input = (ISymbolGroup) group.getData(SymbolLibraryKeys.KEY_GROUP); + initializeViewer(group, input, viewer); + + groupViewers.put(input, viewer); + group.setData(SymbolLibraryKeys.KEY_GALLERY, viewer); + + //System.out.println("initialized group: " + group.getText()); + + return viewer; + } + + void initializeViewer(final PGroup group, final ISymbolGroup input, final GalleryViewer viewer) { + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.BEGINNING).grab(true, false).applyTo(viewer.getControl()); + viewer.addDragSupport(new DragSourceParticipant()); + viewer.setAlign(FlowLayout.Align.Left); + viewer.getDiagram().setHint(SynchronizationHints.ERROR_HANDLER, errorHandler); + + viewer.setContentProvider(new IStructuredContentProvider() { + + /** + * Returns the elements in the input, which must be either an array or a + * Collection. + */ + @Override + public Object[] getElements(Object inputElement) { + if(inputElement == null) return new Object[0]; + return ((ISymbolGroup)inputElement).getItems(); + } + + /** + * This implementation does nothing. + */ + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // do nothing. + } + + /** + * This implementation does nothing. + */ + @Override + public void dispose() { + // do nothing. + } + + }); + viewer.setLabelProvider(new LabelProvider()); + viewer.setInput(input); + + // Add event handler that closes libraries on double clicks into empty + // space in library. + viewer.getCanvasContext().getEventHandlerStack().add(new IEventHandler() { + @Override + public int getEventMask() { + return EventTypes.MouseDoubleClickMask; + } + + @Override + public boolean handleEvent(org.simantics.scenegraph.g2d.events.Event e) { + if (externalEventHandler.handleEvent(e)) + return true; + + if (e instanceof MouseDoubleClickedEvent) { + PickRequest req = new PickRequest(((MouseDoubleClickedEvent) e).controlPosition); + Collection result = new ArrayList(); + DiagramUtils.pick(viewer.getDiagram(), req, result); + if (!result.isEmpty()) + return false; + + //System.out.println("NOTHING CLICKED"); + if (group.isDisposed()) + return false; + group.getDisplay().asyncExec(() -> { + if (group.isDisposed()) + return; + + boolean exp = !group.getExpanded(); + group.setData(KEY_USER_EXPANDED, Boolean.valueOf(exp)); + setGroupExpandedWithoutNotification(group, exp); + refreshScrolledComposite(); + }); + return true; + } + return false; + } + }, 0); + } + + static String toPatternString(String filter) { + return DefaultFilterStrategy.defaultToPatternString(filter, true); + } + + static class SymbolItemFilter extends ViewerFilter { + private final String string; + private final Matcher m; + + public SymbolItemFilter(String string, Pattern pattern) { + this.string = string; + this.m = pattern.matcher(""); + } + + @Override + public boolean select(Viewer viewer, Object parentElement, Object element) { + if (element instanceof ISymbolItem) { + ISymbolItem item = (ISymbolItem) element; + return matchesFilter(item.getName()) || matchesFilter(item.getDescription()); + } else if (element instanceof ISymbolGroup) { + ISymbolGroup group = (ISymbolGroup) element; + return matchesFilter(group.getName()); + } + return false; + } + + private boolean matchesFilter(String str) { + m.reset(str.toLowerCase()); + boolean matches = m.matches(); + //System.out.println(pattern + ": " + str + ": " + (matches ? "PASS" : "FAIL")); + return matches; + } + + @Override + public int hashCode() { + return string == null ? 0 : string.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SymbolItemFilter other = (SymbolItemFilter) obj; + if (string == null) { + if (other.string != null) + return false; + } else if (!string.equals(other.string)) + return false; + return true; + } + } + + static Pattern toPattern(String filterText) { + String regExFilter = toPatternString(filterText); + Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY; + return pattern; + } + + static IFilter composeFilter(final FilterConfiguration config) { + final Mode mode = config.getMode(); + final List patterns = new ArrayList(); + for (GroupFilter f : config.getFilters()) { + if (f.isActive()) + patterns.add(toPattern(f.getFilterText())); + } + return new IFilter() { + @Override + public boolean select(Object toTest) { + if (patterns.isEmpty()) + return true; + + String s = (String) toTest; + switch (mode) { + case AND: + for (Pattern pat : patterns) { + Matcher m = pat.matcher(s.toLowerCase()); + //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL")); + if (!m.matches()) + return false; + } + return true; + case OR: + for (Pattern pat : patterns) { + Matcher m = pat.matcher(s.toLowerCase()); + //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL")); + if (m.matches()) + return true; + } + return false; + default: + throw new Error("Shouldn't happen"); + } + } + }; + } + + void updateFilterConfiguration(FilterConfiguration config) { + this.config = config; + IFilter filter = composeFilter(config); + this.currentGroupFilter = filter; + } + + void applyGroupFilters() { + IFilter groupFilter = this.currentGroupFilter; + final boolean[] changed = new boolean[] { false }; + + Control[] grps = c.getChildren(); + for (Control ctrl : grps) { + final PGroup grp = (PGroup) ctrl; + boolean visible = grp.getVisible(); + boolean shouldBeVisible = groupFilter.select(grp.getText()); + boolean change = visible != shouldBeVisible; + changed[0] |= change; + + grp.setData(KEY_GROUP_FILTERED, Boolean.valueOf(!shouldBeVisible)); + if (change) { + setGroupVisible(grp, shouldBeVisible); + } + } + + ThreadUtils.asyncExec(swtThread, new Runnable() { + @Override + public void run() { + if (c.isDisposed()) + return; + if (changed[0]) { + c.layout(true); + syncRefreshScrolledComposite(); + } + } + }); + } + + /** + * Filters the symbol groups and makes them visible/invisible as necessary. + * Invoke only from the SWT thread. + * + * @param text the filter text given by the client + * @return true if all groups were successfully filtered + * without asynchronous results + */ + boolean filterGroups(String text) { + //System.out.println("FILTERING WITH TEXT: " + text); + + String regExFilter = toPatternString(text); + Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY; + + this.currentFilterPattern = pattern; + final boolean[] changed = new boolean[] { false }; + boolean filteringComplete = true; + + ViewerFilter filter = null; + if (regExFilter != null) + filter = new SymbolItemFilter(regExFilter, pattern); + + Control[] grps = c.getChildren(); + for (Control ctrl : grps) { + final PGroup grp = (PGroup) ctrl; + if (grp.isDisposed()) + continue; + Boolean contentsChanged = filterGroup(grp, filter); + if (contentsChanged == null) + filteringComplete = false; + else + changed[0] = contentsChanged; + } + + ThreadUtils.asyncExec(swtThread, new Runnable() { + @Override + public void run() { + if (c.isDisposed()) + return; + if (changed[0]) { + c.layout(true); + syncRefreshScrolledComposite(); + } + } + }); + + return filteringComplete; + } + + static boolean objectEquals(Object o1, Object o2) { + if (o1==o2) return true; + if (o1==null && o2==null) return true; + if (o1==null || o2==null) return false; + return o1.equals(o2); + } + + /** + * @param grp + * @return true if the filtering caused changes in the group, + * false if not, and null if filtering + * could not be performed yet, meaning results need to be asked + * later + */ + private Boolean filterGroup(PGroup grp, ViewerFilter filter) { + boolean changed = false; + GalleryViewer viewer = initializeGroup(grp); + if (viewer == null) + return null; + + ViewerFilter lastFilter = viewer.getFilter(); + + boolean groupFiltered = Boolean.TRUE.equals(grp.getData(KEY_GROUP_FILTERED)); + boolean userExpanded = Boolean.TRUE.equals(grp.getData(KEY_USER_EXPANDED)); + final boolean expanded = grp.getExpanded(); + final boolean visible = grp.getVisible(); + final boolean filterChanged = !objectEquals(filter, lastFilter); + final ISymbolGroup symbolGroup = (ISymbolGroup) grp.getData(SymbolLibraryKeys.KEY_GROUP); + final boolean filterMatchesGroup = filter != null && filter.select(viewer, null, symbolGroup); + + // Find out how much data would be shown with the new filter. + viewer.setFilter(filterMatchesGroup ? null : filter); + Object[] elements = viewer.getFilteredElements(); + + boolean shouldBeVisible = !groupFiltered && (elements.length > 0 || filterMatchesGroup); + boolean shouldBeExpanded = shouldBeVisible && (filter != null || userExpanded); + +// System.out.format("%40s: filterMatchesGroup(%s) = %s, visible/should be = %5s %5s, expanded/user expanded/should be = %5s %5s %5s\n", +// grp.getText(), +// symbolGroup.getName(), +// String.valueOf(filterMatchesGroup), +// String.valueOf(visible), +// String.valueOf(shouldBeVisible), +// String.valueOf(expanded), +// String.valueOf(userExpanded), +// String.valueOf(shouldBeExpanded)); + + if (filterChanged || visible != shouldBeVisible || expanded != shouldBeExpanded) { + changed = true; + + if (shouldBeVisible == userExpanded) { + if (expanded != shouldBeExpanded) + setGroupExpandedWithoutNotification(grp, shouldBeExpanded); + setGroupVisible(grp, shouldBeVisible); + } else { + if (filter != null) { + if (shouldBeVisible) { + // The user has not expanded this group but the group contains + // stuff that matches the non-empty filter => show the group. + setGroupExpandedWithoutNotification(grp, true); + setGroupVisible(grp, true); + } else { + // The user has expanded this group but it does not contain items + // should should be shown with the current non-empty filter => hide the group. + setGroupExpandedWithoutNotification(grp, true); + setGroupVisible(grp, false); + } + } else { + // All groups should be visible. Some should be expanded and others not. + if (expanded != userExpanded) + setGroupExpandedWithoutNotification(grp, userExpanded); + if (!visible) + setGroupVisible(grp, true); + } + } + + if (shouldBeExpanded) { + viewer.refreshWithContent(elements); + } + } + +// String label = grp.getText(); +// Matcher m = pattern.matcher(label.toLowerCase()); +// boolean visible = m.matches(); +// if (visible != grp.getVisible()) { +// changed = true; +// setGroupVisible(grp, visible); +// } + + return changed; + } + + void setGroupExpandedWithoutNotification(PGroup grp, boolean expanded) { + // Ok, don't need to remove/add expand listener, PGroup will not notify + // listeners when setExpanded is invoked. + //grp.removeExpandListener(groupExpandListener); + storeGroupExpandedState(grp, expanded); + grp.setExpanded(expanded); + //grp.addExpandListener(groupExpandListener); + } + + void setGroupVisible(PGroup group, boolean visible) { + GridData gd = (GridData) group.getLayoutData(); + gd.exclude = !visible; + group.setVisible(visible); + } + + boolean isGroupFiltered(String label) { + return !currentFilterPattern.matcher(label.toLowerCase()).matches(); + } + + class DragSourceParticipant extends AbstractDiagramParticipant implements IDragSourceParticipant { + @Reference Selection selection; + @Dependency PointerInteractor pi; + @Dependency TransformUtil util; + @Dependency PickContext pickContext; + + @Override + public int canDrag(MouseDragBegin me) { + if (me.button != MouseEvent.LEFT_BUTTON) return 0; + if (getHint(Hints.KEY_TOOL) != Hints.POINTERTOOL) return 0; + assertDependencies(); + + PickRequest req = new PickRequest(me.startCanvasPos); + req.pickPolicy = PickRequest.PickPolicy.PICK_INTERSECTING_OBJECTS; + List picks = new ArrayList(); + pickContext.pick(diagram, req, picks); + Set sel = selection.getSelection(me.mouseId); + + if (Collections.disjoint(sel, picks)) return 0; + // Box Select + return DnDConstants.ACTION_COPY; + } + + @Override + public Transferable dragStart(DragGestureEvent e) { + + AWTChassis chassis = (AWTChassis) e.getComponent(); + ICanvasContext cc = chassis.getCanvasContext(); + Selection sel = cc.getSingleItem(Selection.class); + + Set ss = sel.getSelection(0); + if (ss.isEmpty()) return null; + Object[] res = new Object[ss.size()]; + int index = 0; + for (IElement ee : ss) + res[index++] = ee.getHint(ElementHints.KEY_OBJECT); + + ISelection object = new StructuredSelection(res); + + LocalObjectTransferable local = new LocalObjectTransferable(object); + + StringBuilder json = new StringBuilder(); + json.append("{"); + json.append(" \"type\" : \"Symbol\","); + json.append(" \"res\" : ["); + int pos = 0; + for(int i=0;i 0) json.append(","); + Object r = res[i]; + if(r instanceof IAdaptable) { + Resource resource = ((IAdaptable) r).getAdapter(Resource.class); + if(resource != null) { + long rid = resource.getResourceId(); + json.append(Long.toString(rid)); + pos++; + } + } + } + json.append("] }"); + + String jsonText = json.toString(); + StringSelection text = new StringSelection(jsonText); + PlaintextTransfer plainText = new PlaintextTransfer(jsonText); + + return new MultiTransferable(local, text, plainText); + + } + + @Override + public int getAllowedOps() { + return DnDConstants.ACTION_COPY; + } + @Override + public void dragDropEnd(DragSourceDropEvent dsde) { +// System.out.println("dragDropEnd: " + dsde); + LocalObjectTransfer.getTransfer().clear(); + } + @Override + public void dragEnter(DragSourceDragEvent dsde) { + } + @Override + public void dragExit(DragSourceEvent dse) { + } + @Override + public void dragOver(DragSourceDragEvent dsde) { + } + @Override + public void dropActionChanged(DragSourceDragEvent dsde) { + } + } + + ExpandListener groupExpandListener = new ExpandListener() { + @Override + public void itemCollapsed(ExpandEvent e) { + final PGroup group = (PGroup) e.widget; + group.setData(KEY_USER_EXPANDED, Boolean.FALSE); + storeGroupExpandedState(group, false); + //System.out.println("item collapsed: " + group + ", " + sc.getClientArea()); + refreshScrolledComposite(); + } + @Override + public void itemExpanded(ExpandEvent e) { + final PGroup group = (PGroup) e.widget; + group.setData(KEY_USER_EXPANDED, Boolean.TRUE); + storeGroupExpandedState(group, true); + //System.out.println("item expanded: " + group + ", " + sc.getClientArea()); + ThreadUtils.asyncExec(swtThread, () -> { + GalleryViewer viewer = initializeGroup(group); + if (viewer == null) + return; + ThreadUtils.asyncExec(swtThread, () -> { + if (viewer.getControl().isDisposed()) + return; + viewer.refresh(); + refreshScrolledComposite(); + }); + }); + } + }; + + public boolean isDefaultExpanded() { + return defaultExpanded; + } + + public void setDefaultExpanded(boolean defaultExpanded) { + this.defaultExpanded = defaultExpanded; + } + + Runnable disposer(final Widget w) { + return new Runnable() { + @Override + public void run() { + if (w.isDisposed()) + return; + w.dispose(); + } + }; + } + + /** + * Invoke from SWT thread only. + * + * @param targetState + */ + public void setAllExpandedStates(boolean targetState) { + setDefaultExpanded(targetState); + Control[] grps = c.getChildren(); + boolean changed = false; + for (Control control : grps) + changed |= setExpandedState((PGroup) control, targetState, false); + if (changed) + refreshScrolledComposite(); + } + + /** + * Invoke from SWT thread only. + * + * @param grp + * @param targetState + * @return + */ + boolean setExpandedState(PGroup grp, boolean targetState, boolean force) { + if (grp.isDisposed()) + return false; + + storeGroupExpandedState(grp, targetState); + grp.setData(KEY_USER_EXPANDED, Boolean.valueOf(targetState)); + if ((force || grp.getExpanded() != targetState) && grp.getVisible()) { + final GalleryViewer viewer = initializeGroup(grp); + setGroupExpandedWithoutNotification(grp, targetState); + ThreadUtils.asyncExec(swtThread, () -> { + if (!grp.isDisposed()) { + if (viewer != null) + viewer.refresh(); + refreshScrolledComposite(); + } + }); + return true; + } + return false; + } + + class ImageLoader implements Runnable { + + private final ImageProxy imageProxy; + private final ISymbolItem item; + + public ImageLoader(ImageProxy imageProxy, ISymbolItem item) { + this.imageProxy = imageProxy; + this.item = item; + } + + @Override + public void run() { + // SVG images using the SVGUniverse in SVGCache must use + // AWT thread for all operations. + ThreadUtils.asyncExec(AWTThread.getThreadAccess(), () -> runBlocking()); + } + + private void runBlocking() { + try { + ISymbolGroup group = item.getGroup(); + if (group == null) + throw new ProvisionException("No ISymbolGroup available for ISymbolItem " + item); + + GalleryViewer viewer = groupViewers.get(group); + if (viewer == null) { + // This is normal if this composite has been disposed while these are being ran. + //throw new ProvisionException("No GalleryViewer available ISymbolGroup " + group); + imageProxy.setSource(DefaultImages.UNKNOWN2.get()); + return; + } + + IHintContext hints = viewer.getDiagram(); + if (hints == null) + throw new ProvisionException("No diagram available for GalleryViewer of group " + group); + + hints.setHint(ISymbolItem.KEY_ELEMENT_CLASS_LISTENER, new ElementClassListener(imageCache, disposed, item)); + final ElementClass ec = item.getElementClass(hints); + + // Without this the symbol library will at times + // not update the final graphics for the symbol. + // It will keep displaying the hourglass pending icon instead. + symbolUpdate(disposed, imageProxy, ec); + } catch (ProvisionException e) { + ExceptionUtils.logWarning("Failed to provide element class for symbol item " + item, e); + imageProxy.setSource(DefaultImages.ERROR_DECORATOR.get()); + } catch (Exception e) { + ExceptionUtils.logError(e); + imageProxy.setSource(DefaultImages.ERROR_DECORATOR.get()); + } finally { + } + } + } + + static class ElementClassListener implements org.simantics.db.procedure.Listener { + private Map> imageCache; + private final AtomicBoolean disposed; + private final ISymbolItem item; + + public ElementClassListener(Map> imageCache, AtomicBoolean disposed, ISymbolItem item) { + this.imageCache = imageCache; + this.disposed = disposed; + this.item = item; + } + + @Override + public void execute(final ElementClass ec) { + //System.out.println("SYMBOL CHANGED: " + item + " - disposed=" + disposed + " - " + ec); + + final ImageProxy[] imageProxy = { null }; + SoftReference proxyRef = imageCache.get(item); + if (proxyRef != null) + imageProxy[0] = proxyRef.get(); + if (imageProxy[0] != null) + scheduleSymbolUpdate(disposed, imageProxy[0], ec); + } + + @Override + public void exception(Throwable t) { + Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error in ElementClass request.", t)); + } + + @Override + public boolean isDisposed() { + //System.out.println("ElementClassListener.isDisposed " + item + " - " + disposed.get()); + return disposed.get(); + } + } + + public FilterArea getFilterArea() { + return filter; + } + + public static void scheduleSymbolUpdate(final AtomicBoolean disposed, final ImageProxy imageProxy, final ElementClass ec) { + if (disposed.get()) + return; + ThreadUtils.asyncExec(AWTThread.getThreadAccess(), new Runnable() { + @Override + public void run() { + if (disposed.get()) + return; + symbolUpdate(disposed, imageProxy, ec); + } + }); + } + + public static void symbolUpdate(final AtomicBoolean disposed, final ImageProxy imageProxy, final ElementClass ec) { + StaticSymbol ss = ec.getSingleItem(StaticSymbol.class); + Image source = ss == null ? DefaultImages.UNKNOWN2.get() : ss.getImage(); + imageProxy.setSource(source); + } + + Runnable filterActivator = new Runnable() { + @Override + public void run() { + filter.focus(); + } + }; + Listener filterActivationListener = new Listener() { + @Override + public void handleEvent(Event event) { + //System.out.println("event: " + event); + filterActivator.run(); + } + }; + + ISymbolGroupListener groupListener = new ISymbolGroupListener() { + @Override + public void itemsChanged(ISymbolGroup group) { + //System.out.println("symbol group changed: " + group); + GalleryViewer viewer = groupViewers.get(group); + if (viewer != null) { + ISymbolItem[] input = group.getItems(); + viewer.setInput(input); + } + } + }; + + IEventHandler externalEventHandler = new IEventHandler() { + @Override + public int getEventMask() { + return EventTypes.AnyMask; + } + @Override + public boolean handleEvent(org.simantics.scenegraph.g2d.events.Event e) { + IEventHandler handler = SymbolLibraryComposite.this.eventHandler; + return handler != null && EventTypes.passes(handler, e) ? handler.handleEvent(e) : false; + } + }; + + protected volatile IEventHandler eventHandler; + + /** + * @param eventHandler + */ + public void setEventHandler(IEventHandler eventHandler) { + this.eventHandler = eventHandler; + } + + protected void storeGroupExpandedState(PGroup group, boolean expanded) { + ISymbolGroup symbolGroup = (ISymbolGroup) group.getData(SymbolLibraryKeys.KEY_GROUP); + //System.out.println("setGroupExpandedWithoutNotification(" + group + ", " + expanded + ", " + symbolGroup + ")"); + if (symbolGroup != null) { + Object key = symbolGroupToKey(symbolGroup); + expandedGroups.put(key, expanded ? Boolean.TRUE : Boolean.FALSE); + } + } + + private static Object symbolGroupToKey(ISymbolGroup symbolGroup) { + if (symbolGroup instanceof IIdentifiedObject) + return ((IIdentifiedObject) symbolGroup).getId(); + return new Tuple2(symbolGroup.getName(), symbolGroup.getDescription()); + } + +}