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