X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=bundles%2Forg.simantics.diagram%2Fsrc%2Forg%2Fsimantics%2Fdiagram%2Fsymbollibrary%2Fui%2FSymbolLibraryView.java;h=0432f83be482ac1e9a7dbfbc8b19a52088b5d710;hb=1b93154e988c98b4a2be6a1492b6eabc8b0f6471;hp=beb3408eb9ec6ea14fda4d73da321ca76ad22280;hpb=969bd23cab98a79ca9101af33334000879fb60c5;p=simantics%2Fplatform.git diff --git a/bundles/org.simantics.diagram/src/org/simantics/diagram/symbollibrary/ui/SymbolLibraryView.java b/bundles/org.simantics.diagram/src/org/simantics/diagram/symbollibrary/ui/SymbolLibraryView.java index beb3408eb..0432f83be 100644 --- a/bundles/org.simantics.diagram/src/org/simantics/diagram/symbollibrary/ui/SymbolLibraryView.java +++ b/bundles/org.simantics.diagram/src/org/simantics/diagram/symbollibrary/ui/SymbolLibraryView.java @@ -1,1399 +1,1399 @@ -/******************************************************************************* - * 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.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.Executors; -import java.util.concurrent.Semaphore; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.eclipse.core.runtime.preferences.IEclipsePreferences; -import org.eclipse.core.runtime.preferences.InstanceScope; -import org.eclipse.jface.action.Action; -import org.eclipse.jface.action.IToolBarManager; -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.jface.window.Window; -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.eclipse.ui.IMemento; -import org.eclipse.ui.contexts.IContextService; -import org.eclipse.ui.part.ViewPart; -import org.osgi.service.prefs.BackingStoreException; -import org.simantics.db.management.ISessionContextChangedListener; -import org.simantics.db.management.ISessionContextProvider; -import org.simantics.db.management.SessionContextChangedEvent; -import org.simantics.diagram.internal.Activator; -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.ISymbolManager; -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.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.project.IProject; -import org.simantics.project.ProjectKeys; -import org.simantics.scenegraph.INode; -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.ui.SimanticsUI; -import org.simantics.ui.dnd.LocalObjectTransfer; -import org.simantics.ui.dnd.LocalObjectTransferable; -import org.simantics.utils.datastructures.cache.ProvisionException; -import org.simantics.utils.datastructures.hints.HintListenerAdapter; -import org.simantics.utils.datastructures.hints.HintTracker; -import org.simantics.utils.datastructures.hints.IHintContext.Key; -import org.simantics.utils.datastructures.hints.IHintListener; -import org.simantics.utils.datastructures.hints.IHintObservable; -import org.simantics.utils.threads.IThreadWorkQueue; -import org.simantics.utils.threads.SWTThread; -import org.simantics.utils.threads.ThreadUtils; -import org.simantics.utils.ui.BundleUtils; -import org.simantics.utils.ui.ExceptionUtils; -import org.simantics.utils.ui.workbench.StringMemento; - -/** - * @author Tuukka Lehtonen - */ -public class SymbolLibraryView extends ViewPart { - - private static final String SYMBOL_LIBRARY_CONTEXT = "org.simantics.diagram.symbolLibrary"; - private static final String PREF_FILTERS = "filters"; - private static final String TAG_FILTER_MODE = "filterMode"; - private static final String TAG_FILTER = "filter"; - - private static final String ATTR_ACTIVE = "active"; - private static final String ATTR_FILTER_TEXT = "filterText"; - private static final String ATTR_NAME = "name"; - - 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; - ISessionContextProvider sessionCtxProvider; - IThreadWorkQueue swtThread; - boolean defaultExpanded = false; - - /** - * This value is incremented each time a load method is called and symbol - * group population is started. It can be used by - * {@link #populateGroups(ExecutorService, Control, Iterator, IFilter)} to - * tell whether it should stop its population job because a later load - * will override its results anyway. - */ - AtomicInteger loadCount = new AtomicInteger(); - - Map groups = new HashMap(); - Map groupViewers = new HashMap(); - 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 = Executors.newCachedThreadPool(threadFactory); - - /** - * Used to prevent annoying reloading of symbols when groups are closed and - * reopened by not always having to schedule an {@link ImageLoader} in - * {@link LabelProvider#getImage(Object)}. - */ - Map> imageCache = new WeakHashMap>(); - - static final Pattern ANY = Pattern.compile(".*"); - Pattern currentFilterPattern = ANY; - - FilterConfiguration config = new FilterConfiguration(); - IFilter currentGroupFilter = AcceptAllFilter.getInstance(); - - ErrorHandler errorHandler = LogErrorHandler.INSTANCE; - - static class GroupDescriptor { - public final ISymbolGroup lib; - public final String label; - public final PGroup group; - - public GroupDescriptor(ISymbolGroup lib, String label, PGroup group) { - assert(lib != null); - assert(label != null); - this.lib = lib; - this.label = label; - this.group = group; - } - } - - Comparator groupComparator = new Comparator() { - @Override - public int compare(GroupDescriptor o1, GroupDescriptor o2) { - return o1.label.compareToIgnoreCase(o2.label); - } - }; - - static final EnumSet VOLATILE = EnumSet.of(Feature.Volatile); - - static class PendingImage extends ImageProxy { - EnumSet features; - PendingImage(Image source, EnumSet features) { - super(source); - this.features = features; - } - @Override - public EnumSet getFeatures() { - return features; - } - } - - class LabelProvider extends BaseLabelProvider implements ILabelProvider { - @Override - public Image getImage(final Object element) { - ISymbolItem item = (ISymbolItem) element; - // Use a volatile ImageProxy to make the image loading asynchronous. - ImageProxy proxy = null; - SoftReference proxyRef = imageCache.get(item); - if (proxyRef != null) - proxy = proxyRef.get(); - if (proxy == null) { - proxy = new PendingImage(DefaultImages.HOURGLASS.get(), VOLATILE); - imageCache.put(item, new SoftReference(proxy)); - ThreadUtils.getNonBlockingWorkExecutor().schedule(new ImageLoader(proxy, item), 100, TimeUnit.MILLISECONDS); - } - return proxy; - } - @Override - public String getText(final Object element) { - return ((ISymbolItem) element).getName(); - } - @Override - public String getToolTipText(Object element) { - ISymbolItem item = (ISymbolItem) element; - String name = item.getName(); - String desc = item.getDescription(); - return name.equals(desc) ? name : name + " - " + desc; - } - - @Override - public java.awt.Image getToolTipImage(Object object) { - return null; - } - @Override - public Color getToolTipBackgroundColor(Object object) { - return null; - } - - @Override - public Color getToolTipForegroundColor(Object object) { - return null; - } - } - - public class ProjectTracker extends HintTracker { - public ProjectTracker() { - IHintListener symbolGroupListener = new HintListenerAdapter() { - @Override - public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) { - @SuppressWarnings("unchecked") - Collection groups = (Collection) newValue; - load(groups); - } - }; - addKeyHintListener(ISymbolManager.KEY_SYMBOL_GROUPS, symbolGroupListener); - } - } - ProjectTracker projectTracker= new ProjectTracker(); - - public class SessionContextTracker extends HintTracker implements ISessionContextChangedListener { - public SessionContextTracker() { - IHintListener activeProjectListener = new HintListenerAdapter() { - @Override - public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) { - projectTracker.track((IProject) newValue); - } - }; - addKeyHintListener(ProjectKeys.KEY_PROJECT, activeProjectListener); - } - @Override - public void sessionContextChanged(SessionContextChangedEvent event) { - track(event.getNewValue()); - } - } - SessionContextTracker sessionContextTracker = new SessionContextTracker(); - - void attachToSession() { - // Track active ISessionContext changes - sessionCtxProvider = SimanticsUI.getSessionContextProvider(getViewSite().getWorkbenchWindow()); - sessionCtxProvider.addContextChangedListener(sessionContextTracker); - - // Start tracking the current session context for input changes. - // This will/must cause applySessionContext to get called. - // Doing the applySessionContext initialization this way - // instead of directly calling it will also make sure that - // applySessionContext is only called once when first initialized, - // and not twice like with the direct invocation. - sessionContextTracker.track(sessionCtxProvider.getSessionContext()); - } - - public void readPreferences() { - FilterConfiguration config = new FilterConfiguration(); - - IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(Activator.PLUGIN_ID); - String filters = prefs.get(PREF_FILTERS, null); - - if (filters != null) { - IMemento memento = new StringMemento(filters); - for (IMemento child : memento.getChildren(TAG_FILTER)) { - String name = child.getString(ATTR_NAME); - String filterText = child.getString(ATTR_FILTER_TEXT); - boolean active = Boolean.TRUE.equals(child.getBoolean(ATTR_ACTIVE)); - - if (name != null && !name.trim().isEmpty() && filterText != null && !filterText.isEmpty()) { - config.getFilters().add(new GroupFilter(name, filterText, active)); - } - } - Collections.sort(config.getFilters()); - - String filterMode = memento.getString(TAG_FILTER_MODE); - if (filterMode != null) { - try { - Mode mode = Mode.valueOf(filterMode); - config.setMode(mode); - } catch (IllegalArgumentException e) { - } - } - } - - updateFilterConfiguration(config); - } - - void savePreferences() throws BackingStoreException { - IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(Activator.PLUGIN_ID); - - IMemento memento = new StringMemento(); - for (GroupFilter f : config.getFilters()) { - IMemento child = memento.createChild(TAG_FILTER); - child.putString(ATTR_NAME, f.getName()); - child.putString(ATTR_FILTER_TEXT, f.getFilterText()); - child.putBoolean(ATTR_ACTIVE, f.isActive()); - } - memento.putString(TAG_FILTER_MODE, config.getMode().toString()); - - prefs.put(PREF_FILTERS, memento.toString()); - - prefs.flush(); - } - - @Override - public void createPartControl(final Composite parent) { - // Prime the image, make it available. - DefaultImages.HOURGLASS.get(); - - readPreferences(); - - this.resourceManager = new LocalResourceManager(JFaceResources.getResources(parent.getDisplay()), parent); - swtThread = SWTThread.getThreadAccess(parent); - - GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(parent); - - filter = new FilterArea(parent, 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(parent, 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(); - } - }); - - c = new Composite(sc, 0); - c.setVisible(false); - GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(c); - - sc.setContent(c); - - c.addDisposeListener(new DisposeListener() { - @Override - public void widgetDisposed(DisposeEvent e) { - // These should be in exactly this order to prevent them from - // screwing each other up. - sessionContextTracker.untrack(); - projectTracker.untrack(); - - // Remember to shutdown the executor - loaderExecutor.shutdown(); - }}); - - contributeActions(); - attachToSession(); - - IContextService cs = (IContextService) getSite().getService(IContextService.class); - cs.activateContext(SYMBOL_LIBRARY_CONTEXT); - } - - @Override - public void dispose() { - if (sessionCtxProvider != null) { - sessionCtxProvider.removeContextChangedListener(sessionContextTracker); - sessionCtxProvider = null; - } - } - - void refreshScrolledComposite() { - // Execute asynchronously to give the UI events triggering this method - // call time to run through before actually doing any resizing. - // Otherwise the result will lag behind reality when scrollbar - // visibility is toggled by the toolkit. - ThreadUtils.asyncExec(swtThread, new Runnable() { - @Override - public void run() { - if (sc.isDisposed()) - return; - syncRefreshScrolledComposite(); - } - }); - } - - void syncRefreshScrolledComposite() { - // Execute asynchronously to give the UI events triggering this method - // call time to run through before actually doing any resizing. - // Otherwise the result will lag behind reality when scrollbar - // visibility is toggled by the toolkit. - Rectangle r = sc.getClientArea(); - Point contentSize = c.computeSize(r.width, SWT.DEFAULT); - //System.out.println("[" + Thread.currentThread() + "] computed content size: " + contentSize + ", " + r); - c.setSize(contentSize); - } - - /** - * (Re-)Load symbol groups, refresh the content - */ - void load(Collection _libraries) { - if (_libraries == null) - _libraries = Collections.emptyList(); - final Collection libraries = _libraries; - loaderExecutor.execute(new Runnable() { - @Override - public void run() { - // Increment loadCount to signal that a new load cycle is on the way. - Integer loadId = loadCount.incrementAndGet(); - try { - loaderSemaphore.acquire(); - beginPopulate(loadId); - } catch (InterruptedException e) { - ExceptionUtils.logError(e); - } catch (RuntimeException e) { - loaderSemaphore.release(); - ExceptionUtils.logAndShowError(e); - } catch (Error e) { - loaderSemaphore.release(); - ExceptionUtils.logAndShowError(e); - } - } - - void beginPopulate(Integer loadId) { - synchronized (groups) { - // Must use toArray since groups are removed within the loop - for (Iterator> it = groups.entrySet().iterator(); it.hasNext();) { - Map.Entry entry = it.next(); - if (!libraries.contains(entry.getKey())) { - PGroup group = entry.getValue(); - it.remove(); - groupViewers.remove(entry.getKey()); - if (group != null && !group.isDisposed()) - ThreadUtils.asyncExec(swtThread, disposer(group)); - } - } - Set groupDescs = new TreeSet(groupComparator); - for (ISymbolGroup lib : libraries) { - PGroup group = groups.get(lib); - //String label = group != null ? group.getText() : lib.getName(); - String label = lib.getName(); - groupDescs.add(new GroupDescriptor(lib, label, group)); - } - - // Populate all the missing groups. - IFilter groupFilter = currentGroupFilter; - populateGroups( - loaderExecutor, - null, - groupDescs.iterator(), - groupFilter, - loadId, - new Runnable() { - @Override - public void run() { - loaderSemaphore.release(); - } - }); - } - } - }); - } - - void populateGroups( - final ExecutorService exec, - final Control lastGroup, - final Iterator iter, - final IFilter groupFilter, - final Integer loadId, - final Runnable loadComplete) - { - // Check whether to still continue this population or not. - int currentLoadId = loadCount.get(); - if (currentLoadId != loadId) { - loadComplete.run(); - return; - } - - if (!iter.hasNext()) { - ThreadUtils.asyncExec(swtThread, new Runnable() { - @Override - public void run() { - if (filter.isDisposed() || c.isDisposed()) - return; - filter.focus(); - c.setVisible(true); - } - }); - loadComplete.run(); - return; - } - - final GroupDescriptor desc = iter.next(); - - ThreadUtils.asyncExec(swtThread, new Runnable() { - @Override - public void run() { - // Must make sure that loadComplete is invoked under error - // circumstances. - try { - populateGroup(); - } catch (RuntimeException e) { - loadComplete.run(); - ExceptionUtils.logAndShowError(e); - } catch (Error e) { - loadComplete.run(); - ExceptionUtils.logAndShowError(e); - } - } - - public void populateGroup() { - if (c.isDisposed()) { - loaderSemaphore.release(); - return; - } - // $ SWT-begin - //System.out.println("populating: " + desc.label); - PGroup group = desc.group; - 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); - } - - groups.put(desc.lib, group); - - group.setData(SymbolLibraryKeys.KEY_GROUP, desc.lib); - group.setData(KEY_USER_EXPANDED, defaultExpanded); - - group.setExpanded(defaultExpanded); - 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); - } - } - - group.setText(desc.label); - group.setToolTipText(desc.label); - - // Initialize the group: set content provider, label provider and input. - // NOTE: this should not yet start loading any data, just setup the viewer. - // [Tuukka @ 2009-10-24] changed group contents to be - // initialized lazily when needed. See references to initializeGroup(PGroup). - //initializeGroup(group); - - // 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; - exec.execute(new Runnable() { - @Override - public void run() { - populateGroups(exec, group_, iter, groupFilter, loadId, loadComplete); - } - }); - } - }); - } - - /** - * @param group - * @return null if GalleryViewer is currently being created - */ - GalleryViewer initializeGroup(final PGroup group) { - if (group.isDisposed()) - return null; - - //System.out.println("initializeGroup(" + group + ")"); - - 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 wait until the SWT/AWT UI population has completed - // and it will dispatch SWT events while waiting for AWT thread population - // to complete. This may in turn cause other parties to invoke this same - // initializeGroup method with the same group as parameter. This case - // needs to be handled appropriately by callers. - GalleryViewer viewer = new GalleryViewer(group); - - ISymbolGroup input = (ISymbolGroup) group.getData(SymbolLibraryKeys.KEY_GROUP); - initializeViewer(group, input, viewer); - - groupViewers.put(input, viewer); - group.setData(SymbolLibraryKeys.KEY_GALLERY, viewer); - - //System.out.println("initialized group: " + group.getText()); - - return viewer; - } - - void initializeViewer(final PGroup group, final ISymbolGroup input, final GalleryViewer viewer) { - GridDataFactory.fillDefaults().align(SWT.FILL, SWT.BEGINNING).grab(true, false).applyTo(viewer.getControl()); - viewer.addDragSupport(new DragSourceParticipant()); - viewer.setAlign(FlowLayout.Align.Left); - viewer.getDiagram().setHint(SynchronizationHints.ERROR_HANDLER, errorHandler); - - viewer.setContentProvider(new IStructuredContentProvider() { - - /** - * Returns the elements in the input, which must be either an array or a - * Collection. - */ - @Override - public Object[] getElements(Object inputElement) { - if(inputElement == null) return new Object[0]; - return ((ISymbolGroup)inputElement).getItems(); - } - - /** - * This implementation does nothing. - */ - @Override - public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { - // do nothing. - } - - /** - * This implementation does nothing. - */ - @Override - public void dispose() { - // do nothing. - } - - }); - viewer.setLabelProvider(new LabelProvider()); - viewer.setInput(input); - - // Add event handler that closes libraries on double clicks into empty - // space in library. - viewer.getCanvasContext().getEventHandlerStack().add(new IEventHandler() { - @Override - public int getEventMask() { - return EventTypes.MouseDoubleClickMask; - } - @Override - public boolean handleEvent(org.simantics.scenegraph.g2d.events.Event e) { - if (e instanceof MouseDoubleClickedEvent) { - PickRequest req = new PickRequest(((MouseDoubleClickedEvent) e).controlPosition); - Collection result = new ArrayList(); - DiagramUtils.pick(viewer.getDiagram(), req, result); - if (!result.isEmpty()) - return false; - - //System.out.println("NOTHING CLICKED"); - if (group.isDisposed()) - return false; - group.getDisplay().asyncExec(new Runnable() { - @Override - public void run() { - 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) { - if (!filter.isEmpty()) { - // Force searching in lowercase. - filter = filter.toLowerCase(); - - // Construct a regular expression from the specified text. - String regExFilter = filter - .replace("\\", "\\\\") // \ -> \\ - .replace(".", "\\.") // . -> \. - .replace("*", ".*") // * -> Any 0..n characters - .replace("?", ".") // ? -> Any single character - .replace("+", "\\+") // + -> \+ - .replace("(", "\\(") // ( -> \( - .replace(")", "\\)") // ) -> \) - .replace("[", "\\[") // [ -> \[ - .replace("]", "\\]") // ] -> \] - .replace("{", "\\{") // { -> \{ - .replace("}", "\\}") // } -> \} - .replace("^", "\\^") // ^ -> \^ - .replace("$", "\\$") // $ -> \$ - .replace("|", ".*|") // $ -> \$ - //.replace("|", "\\|") // | -> \| - .replace("&&", "\\&&") // && -> \&& - ; - - if (!regExFilter.startsWith(".*")) - regExFilter = ".*" + regExFilter ; - if (!regExFilter.endsWith(".*")) - regExFilter += ".*" ; - - return regExFilter; - } - return null; - } - - static class SymbolItemFilter extends ViewerFilter { - private final String string; - private final Pattern pattern; - - public SymbolItemFilter(String string, Pattern pattern) { - this.string = string; - this.pattern = pattern; - } - - @Override - public boolean select(Viewer viewer, Object parentElement, Object element) { - ISymbolItem item = (ISymbolItem) element; - String name = item.getName(); - Matcher m = pattern.matcher(name.toLowerCase()); - //System.out.println(name + ": " + (m.matches() ? "PASS" : "FAIL")); - return m.matches(); - } - - @Override - public int hashCode() { - return string == null ? 0 : string.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - SymbolItemFilter other = (SymbolItemFilter) obj; - if (string == null) { - if (other.string != null) - return false; - } else if (!string.equals(other.string)) - return false; - return true; - } - } - - static Pattern toPattern(String filterText) { - String regExFilter = toPatternString(filterText); - Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY; - return pattern; - } - - static IFilter composeFilter(final FilterConfiguration config) { - final Mode mode = config.getMode(); - final List patterns = new ArrayList(); - for (GroupFilter f : config.getFilters()) { - if (f.isActive()) - patterns.add(toPattern(f.getFilterText())); - } - return new IFilter() { - @Override - public boolean select(Object toTest) { - if (patterns.isEmpty()) - return true; - - String s = (String) toTest; - switch (mode) { - case AND: - for (Pattern pat : patterns) { - Matcher m = pat.matcher(s.toLowerCase()); - //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL")); - if (!m.matches()) - return false; - } - return true; - case OR: - for (Pattern pat : patterns) { - Matcher m = pat.matcher(s.toLowerCase()); - //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL")); - if (m.matches()) - return true; - } - return false; - default: - throw new Error("Shouldn't happen"); - } - } - }; - } - - void updateFilterConfiguration(FilterConfiguration config) { - this.config = config; - IFilter filter = composeFilter(config); - this.currentGroupFilter = filter; - } - - void applyGroupFilters() { - IFilter groupFilter = this.currentGroupFilter; - final boolean[] changed = new boolean[] { false }; - - Control[] grps = c.getChildren(); - for (Control ctrl : grps) { - final PGroup grp = (PGroup) ctrl; - boolean visible = grp.getVisible(); - boolean shouldBeVisible = groupFilter.select(grp.getText()); - boolean change = visible != shouldBeVisible; - changed[0] |= change; - - grp.setData(KEY_GROUP_FILTERED, Boolean.valueOf(!shouldBeVisible)); - if (change) { - setGroupVisible(grp, shouldBeVisible); - } - } - - ThreadUtils.asyncExec(swtThread, new Runnable() { - @Override - public void run() { - if (c.isDisposed()) - return; - if (changed[0]) { - c.layout(true); - syncRefreshScrolledComposite(); - } - } - }); - } - - /** - * Filters the symbol groups and makes them visible/invisible as necessary. - * Invoke only from the SWT thread. - * - * @param text the filter text given by the client - * @return true if all groups were successfully filtered - * without asynchronous results - */ - boolean filterGroups(String text) { - //System.out.println("FILTERING WITH TEXT: " + text); - - String regExFilter = toPatternString(text); - Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY; - - this.currentFilterPattern = pattern; - final boolean[] changed = new boolean[] { false }; - boolean filteringComplete = true; - - ViewerFilter filter = null; - if (regExFilter != null) - filter = new SymbolItemFilter(regExFilter, pattern); - - Control[] grps = c.getChildren(); - for (Control ctrl : grps) { - final PGroup grp = (PGroup) ctrl; - if (grp.isDisposed()) - continue; - Boolean contentsChanged = filterGroup(grp, filter); - if (contentsChanged == null) - filteringComplete = false; - else - changed[0] = contentsChanged; - } - - ThreadUtils.asyncExec(swtThread, new Runnable() { - @Override - public void run() { - if (c.isDisposed()) - return; - if (changed[0]) { - c.layout(true); - syncRefreshScrolledComposite(); - } - } - }); - - return filteringComplete; - } - - static boolean objectEquals(Object o1, Object o2) { - if (o1==o2) return true; - if (o1==null && o2==null) return true; - if (o1==null || o2==null) return false; - return o1.equals(o2); - } - - /** - * @param grp - * @return true if the filtering caused changes in the group, - * false if not, and null if filtering - * could not be performed yet, meaning results need to be asked - * later - */ - private Boolean filterGroup(PGroup grp, ViewerFilter filter) { - boolean changed = false; - GalleryViewer viewer = initializeGroup(grp); - if (viewer == null) - return null; - - ViewerFilter lastFilter = viewer.getFilter(); - - boolean groupFiltered = Boolean.TRUE.equals(grp.getData(KEY_GROUP_FILTERED)); - boolean userExpanded = Boolean.TRUE.equals(grp.getData(KEY_USER_EXPANDED)); - final boolean expanded = grp.getExpanded(); - final boolean visible = grp.getVisible(); - final boolean filterChanged = !objectEquals(filter, lastFilter); - - // Find out how much data would be shown with the new filter. - viewer.setFilter(filter); - Object[] elements = viewer.getFilteredElements(); - - boolean shouldBeVisible = !groupFiltered && elements.length > 0; - boolean shouldBeExpanded = shouldBeVisible && (filter != null || userExpanded); - -// System.out.format("%40s: visible/should be = %5s %5s, expanded/user expanded/should be = %5s %5s %5s\n", -// grp.getText(), -// String.valueOf(visible), -// String.valueOf(shouldBeVisible), -// String.valueOf(expanded), -// String.valueOf(userExpanded), -// String.valueOf(shouldBeExpanded)); - - if (filterChanged || visible != shouldBeVisible || expanded != shouldBeExpanded) { - changed = true; - - if (shouldBeVisible == userExpanded) { - if (expanded != shouldBeExpanded) - setGroupExpandedWithoutNotification(grp, shouldBeExpanded); - setGroupVisible(grp, shouldBeVisible); - } else { - if (filter != null) { - if (shouldBeVisible) { - // The user has not expanded this group but the group contains - // stuff that matches the non-empty filter => show the group. - setGroupExpandedWithoutNotification(grp, true); - setGroupVisible(grp, true); - } else { - // The user has expanded this group but it does not contain items - // should should be shown with the current non-empty filter => hide the group. - setGroupExpandedWithoutNotification(grp, true); - setGroupVisible(grp, false); - } - } else { - // All groups should be visible. Some should be expanded and others not. - if (expanded != userExpanded) - setGroupExpandedWithoutNotification(grp, userExpanded); - if (!visible) - setGroupVisible(grp, true); - } - } - - if (shouldBeExpanded) { - viewer.refreshWithContent(elements); - } - } - -// String label = grp.getText(); -// Matcher m = pattern.matcher(label.toLowerCase()); -// boolean visible = m.matches(); -// if (visible != grp.getVisible()) { -// changed = true; -// setGroupVisible(grp, visible); -// } - - return changed; - } - - void setGroupExpandedWithoutNotification(PGroup grp, boolean expanded) { - // Ok, don't need to remove/add expand listener, PGroup will not notify - // listeners when setExpanded is invoked. - //grp.removeExpandListener(groupExpandListener); - grp.setExpanded(expanded); - //grp.addExpandListener(groupExpandListener); - } - - void setGroupVisible(PGroup group, boolean visible) { - GridData gd = (GridData) group.getLayoutData(); - gd.exclude = !visible; - group.setVisible(visible); - } - - boolean isGroupFiltered(String label) { - return !currentFilterPattern.matcher(label.toLowerCase()).matches(); - } - - class DragSourceParticipant extends AbstractDiagramParticipant implements IDragSourceParticipant { - @Reference Selection selection; - @Dependency PointerInteractor pi; - @Dependency TransformUtil util; - @Dependency PickContext pickContext; - - @Override - public int canDrag(MouseDragBegin me) { - if (me.button != MouseEvent.LEFT_BUTTON) return 0; - if (getHint(Hints.KEY_TOOL) != Hints.POINTERTOOL) return 0; - assertDependencies(); - - PickRequest req = new PickRequest(me.startCanvasPos); - req.pickPolicy = PickRequest.PickPolicy.PICK_INTERSECTING_OBJECTS; - List picks = new ArrayList(); - pickContext.pick(diagram, req, picks); - Set sel = selection.getSelection(me.mouseId); - - if (Collections.disjoint(sel, picks)) return 0; - // Box Select - return DnDConstants.ACTION_LINK; - } - - @Override - public Transferable dragStart(DragGestureEvent e) { - AWTChassis chassis = (AWTChassis) e.getComponent(); - ICanvasContext cc = chassis.getCanvasContext(); - Selection sel = cc.getSingleItem(Selection.class); - - Set ss = sel.getSelection(0); - if (ss.isEmpty()) return null; - Object[] res = new Object[ss.size()]; - int index = 0; - for (IElement ee : ss) - res[index++] = ee.getHint(ElementHints.KEY_OBJECT); - - ISelection object = new StructuredSelection(res); - - return new LocalObjectTransferable(object); - } - - @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); - //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); - //System.out.println("item expanded: " + group + ", " + sc.getClientArea()); - final GalleryViewer viewer = initializeGroup(group); - if (viewer == null) - return; - ThreadUtils.asyncExec(swtThread, new Runnable() { - @Override - public void run() { - viewer.refresh(); - refreshScrolledComposite(); - } - }); - } - }; - - @Override - public void setFocus() { - c.setFocus(); - } - - 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(); - } - }; - } - - void contributeActions() { - IToolBarManager toolbar = getViewSite().getActionBars().getToolBarManager(); - toolbar.add(new Action("Collapse All", - BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/collapseall.gif")) { - @Override - public void run() { - setAllExpandedStates(false); - } - }); - toolbar.add(new Action("Expand All", - BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/expandall.gif")) { - @Override - public void run() { - setAllExpandedStates(true); - } - }); - toolbar.add(new Action("Configure Filters", - BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/filter_ps.gif")) { - @Override - public void run() { - FilterConfiguration clone = new FilterConfiguration(config); - FilterDialog fd = new FilterDialog(getViewSite().getShell(), Activator.getDefault().getDialogSettings(), clone); - int result = fd.open(); - if (result != Window.OK) - return; - - updateFilterConfiguration(clone); - applyGroupFilters(); - - try { - savePreferences(); - } catch (BackingStoreException e) { - ExceptionUtils.logAndShowError(e); - } - } - }); - } - - void setAllExpandedStates(boolean targetState) { - Boolean targetStateObj = Boolean.valueOf(targetState); - boolean changed = false; - setDefaultExpanded(targetState); - Control[] grps = c.getChildren(); - //for (final PGroup grp : groups.values().toArray(new PGroup[0])) { - for (Control control : grps) { - final PGroup grp = (PGroup) control; - grp.setData(KEY_USER_EXPANDED, targetStateObj); - if (!grp.isDisposed() && grp.getExpanded() != targetState && grp.getVisible()) { - final GalleryViewer viewer = initializeGroup(grp); - setGroupExpandedWithoutNotification(grp, targetState); - ThreadUtils.asyncExec(swtThread, new Runnable() { - @Override - public void run() { - if (!grp.isDisposed()) { - if (viewer != null) - viewer.refresh(); - refreshScrolledComposite(); - } - } - }); - changed = true; - } - } - if (changed) - refreshScrolledComposite(); - } - - 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() { - ThreadUtils.getBlockingWorkExecutor().execute(new Runnable() { - @Override - public void run() { - runBlocking(); - } - }); - } - - private void runBlocking() { - try { - IHintObservable hints = null; - 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; - } - - hints = viewer.getDiagram(); - if (hints == null) - throw new ProvisionException("No diagram available for GalleryViewer of group " + group); - - ElementClass ec = item.getElementClass(hints); - StaticSymbol ss = ec.getSingleItem(StaticSymbol.class); - Image source = ss == null ? DefaultImages.UNKNOWN2.get() : ss.getImage(); - imageProxy.setSource(source); - } 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 { - } - } - } - - public FilterArea getFilterArea() { - return filter; - } - - 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) { - GalleryViewer viewer = groupViewers.get(group); - if (viewer != null) { - ISymbolItem[] input = group.getItems(); - viewer.setInput(input); - } - } - }; - - @Override - @SuppressWarnings("rawtypes") - public Object getAdapter(Class adapter) { - // For supporting Scene Graph viewer - if (adapter == INode[].class) { - List result = new ArrayList(groupViewers.size()); - for (GalleryViewer viewer : groupViewers.values()) { - result.add(viewer.getCanvasContext().getSceneGraph()); - } - return result.toArray(new INode[result.size()]); - } - return super.getAdapter(adapter); - } - -} +/******************************************************************************* + * 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.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.Executors; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IToolBarManager; +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.jface.window.Window; +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.eclipse.ui.IMemento; +import org.eclipse.ui.contexts.IContextService; +import org.eclipse.ui.part.ViewPart; +import org.osgi.service.prefs.BackingStoreException; +import org.simantics.db.management.ISessionContextChangedListener; +import org.simantics.db.management.ISessionContextProvider; +import org.simantics.db.management.SessionContextChangedEvent; +import org.simantics.diagram.internal.Activator; +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.ISymbolManager; +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.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.project.IProject; +import org.simantics.project.ProjectKeys; +import org.simantics.scenegraph.INode; +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.ui.SimanticsUI; +import org.simantics.ui.dnd.LocalObjectTransfer; +import org.simantics.ui.dnd.LocalObjectTransferable; +import org.simantics.utils.datastructures.cache.ProvisionException; +import org.simantics.utils.datastructures.hints.HintListenerAdapter; +import org.simantics.utils.datastructures.hints.HintTracker; +import org.simantics.utils.datastructures.hints.IHintContext.Key; +import org.simantics.utils.datastructures.hints.IHintListener; +import org.simantics.utils.datastructures.hints.IHintObservable; +import org.simantics.utils.threads.IThreadWorkQueue; +import org.simantics.utils.threads.SWTThread; +import org.simantics.utils.threads.ThreadUtils; +import org.simantics.utils.ui.BundleUtils; +import org.simantics.utils.ui.ExceptionUtils; +import org.simantics.utils.ui.workbench.StringMemento; + +/** + * @author Tuukka Lehtonen + */ +public class SymbolLibraryView extends ViewPart { + + private static final String SYMBOL_LIBRARY_CONTEXT = "org.simantics.diagram.symbolLibrary"; + private static final String PREF_FILTERS = "filters"; + private static final String TAG_FILTER_MODE = "filterMode"; + private static final String TAG_FILTER = "filter"; + + private static final String ATTR_ACTIVE = "active"; + private static final String ATTR_FILTER_TEXT = "filterText"; + private static final String ATTR_NAME = "name"; + + 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; + ISessionContextProvider sessionCtxProvider; + IThreadWorkQueue swtThread; + boolean defaultExpanded = false; + + /** + * This value is incremented each time a load method is called and symbol + * group population is started. It can be used by + * {@link #populateGroups(ExecutorService, Control, Iterator, IFilter)} to + * tell whether it should stop its population job because a later load + * will override its results anyway. + */ + AtomicInteger loadCount = new AtomicInteger(); + + Map groups = new HashMap(); + Map groupViewers = new HashMap(); + 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 = Executors.newCachedThreadPool(threadFactory); + + /** + * Used to prevent annoying reloading of symbols when groups are closed and + * reopened by not always having to schedule an {@link ImageLoader} in + * {@link LabelProvider#getImage(Object)}. + */ + Map> imageCache = new WeakHashMap>(); + + static final Pattern ANY = Pattern.compile(".*"); + Pattern currentFilterPattern = ANY; + + FilterConfiguration config = new FilterConfiguration(); + IFilter currentGroupFilter = AcceptAllFilter.getInstance(); + + ErrorHandler errorHandler = LogErrorHandler.INSTANCE; + + static class GroupDescriptor { + public final ISymbolGroup lib; + public final String label; + public final PGroup group; + + public GroupDescriptor(ISymbolGroup lib, String label, PGroup group) { + assert(lib != null); + assert(label != null); + this.lib = lib; + this.label = label; + this.group = group; + } + } + + Comparator groupComparator = new Comparator() { + @Override + public int compare(GroupDescriptor o1, GroupDescriptor o2) { + return o1.label.compareToIgnoreCase(o2.label); + } + }; + + static final EnumSet VOLATILE = EnumSet.of(Feature.Volatile); + + static class PendingImage extends ImageProxy { + EnumSet features; + PendingImage(Image source, EnumSet features) { + super(source); + this.features = features; + } + @Override + public EnumSet getFeatures() { + return features; + } + } + + class LabelProvider extends BaseLabelProvider implements ILabelProvider { + @Override + public Image getImage(final Object element) { + ISymbolItem item = (ISymbolItem) element; + // Use a volatile ImageProxy to make the image loading asynchronous. + ImageProxy proxy = null; + SoftReference proxyRef = imageCache.get(item); + if (proxyRef != null) + proxy = proxyRef.get(); + if (proxy == null) { + proxy = new PendingImage(DefaultImages.HOURGLASS.get(), VOLATILE); + imageCache.put(item, new SoftReference(proxy)); + ThreadUtils.getNonBlockingWorkExecutor().schedule(new ImageLoader(proxy, item), 100, TimeUnit.MILLISECONDS); + } + return proxy; + } + @Override + public String getText(final Object element) { + return ((ISymbolItem) element).getName(); + } + @Override + public String getToolTipText(Object element) { + ISymbolItem item = (ISymbolItem) element; + String name = item.getName(); + String desc = item.getDescription(); + return name.equals(desc) ? name : name + " - " + desc; + } + + @Override + public java.awt.Image getToolTipImage(Object object) { + return null; + } + @Override + public Color getToolTipBackgroundColor(Object object) { + return null; + } + + @Override + public Color getToolTipForegroundColor(Object object) { + return null; + } + } + + public class ProjectTracker extends HintTracker { + public ProjectTracker() { + IHintListener symbolGroupListener = new HintListenerAdapter() { + @Override + public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) { + @SuppressWarnings("unchecked") + Collection groups = (Collection) newValue; + load(groups); + } + }; + addKeyHintListener(ISymbolManager.KEY_SYMBOL_GROUPS, symbolGroupListener); + } + } + ProjectTracker projectTracker= new ProjectTracker(); + + public class SessionContextTracker extends HintTracker implements ISessionContextChangedListener { + public SessionContextTracker() { + IHintListener activeProjectListener = new HintListenerAdapter() { + @Override + public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) { + projectTracker.track((IProject) newValue); + } + }; + addKeyHintListener(ProjectKeys.KEY_PROJECT, activeProjectListener); + } + @Override + public void sessionContextChanged(SessionContextChangedEvent event) { + track(event.getNewValue()); + } + } + SessionContextTracker sessionContextTracker = new SessionContextTracker(); + + void attachToSession() { + // Track active ISessionContext changes + sessionCtxProvider = SimanticsUI.getSessionContextProvider(getViewSite().getWorkbenchWindow()); + sessionCtxProvider.addContextChangedListener(sessionContextTracker); + + // Start tracking the current session context for input changes. + // This will/must cause applySessionContext to get called. + // Doing the applySessionContext initialization this way + // instead of directly calling it will also make sure that + // applySessionContext is only called once when first initialized, + // and not twice like with the direct invocation. + sessionContextTracker.track(sessionCtxProvider.getSessionContext()); + } + + public void readPreferences() { + FilterConfiguration config = new FilterConfiguration(); + + IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(Activator.PLUGIN_ID); + String filters = prefs.get(PREF_FILTERS, null); + + if (filters != null) { + IMemento memento = new StringMemento(filters); + for (IMemento child : memento.getChildren(TAG_FILTER)) { + String name = child.getString(ATTR_NAME); + String filterText = child.getString(ATTR_FILTER_TEXT); + boolean active = Boolean.TRUE.equals(child.getBoolean(ATTR_ACTIVE)); + + if (name != null && !name.trim().isEmpty() && filterText != null && !filterText.isEmpty()) { + config.getFilters().add(new GroupFilter(name, filterText, active)); + } + } + Collections.sort(config.getFilters()); + + String filterMode = memento.getString(TAG_FILTER_MODE); + if (filterMode != null) { + try { + Mode mode = Mode.valueOf(filterMode); + config.setMode(mode); + } catch (IllegalArgumentException e) { + } + } + } + + updateFilterConfiguration(config); + } + + void savePreferences() throws BackingStoreException { + IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(Activator.PLUGIN_ID); + + IMemento memento = new StringMemento(); + for (GroupFilter f : config.getFilters()) { + IMemento child = memento.createChild(TAG_FILTER); + child.putString(ATTR_NAME, f.getName()); + child.putString(ATTR_FILTER_TEXT, f.getFilterText()); + child.putBoolean(ATTR_ACTIVE, f.isActive()); + } + memento.putString(TAG_FILTER_MODE, config.getMode().toString()); + + prefs.put(PREF_FILTERS, memento.toString()); + + prefs.flush(); + } + + @Override + public void createPartControl(final Composite parent) { + // Prime the image, make it available. + DefaultImages.HOURGLASS.get(); + + readPreferences(); + + this.resourceManager = new LocalResourceManager(JFaceResources.getResources(parent.getDisplay()), parent); + swtThread = SWTThread.getThreadAccess(parent); + + GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(parent); + + filter = new FilterArea(parent, 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(parent, 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(); + } + }); + + c = new Composite(sc, 0); + c.setVisible(false); + GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(c); + + sc.setContent(c); + + c.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + // These should be in exactly this order to prevent them from + // screwing each other up. + sessionContextTracker.untrack(); + projectTracker.untrack(); + + // Remember to shutdown the executor + loaderExecutor.shutdown(); + }}); + + contributeActions(); + attachToSession(); + + IContextService cs = (IContextService) getSite().getService(IContextService.class); + cs.activateContext(SYMBOL_LIBRARY_CONTEXT); + } + + @Override + public void dispose() { + if (sessionCtxProvider != null) { + sessionCtxProvider.removeContextChangedListener(sessionContextTracker); + sessionCtxProvider = null; + } + } + + void refreshScrolledComposite() { + // Execute asynchronously to give the UI events triggering this method + // call time to run through before actually doing any resizing. + // Otherwise the result will lag behind reality when scrollbar + // visibility is toggled by the toolkit. + ThreadUtils.asyncExec(swtThread, new Runnable() { + @Override + public void run() { + if (sc.isDisposed()) + return; + syncRefreshScrolledComposite(); + } + }); + } + + void syncRefreshScrolledComposite() { + // Execute asynchronously to give the UI events triggering this method + // call time to run through before actually doing any resizing. + // Otherwise the result will lag behind reality when scrollbar + // visibility is toggled by the toolkit. + Rectangle r = sc.getClientArea(); + Point contentSize = c.computeSize(r.width, SWT.DEFAULT); + //System.out.println("[" + Thread.currentThread() + "] computed content size: " + contentSize + ", " + r); + c.setSize(contentSize); + } + + /** + * (Re-)Load symbol groups, refresh the content + */ + void load(Collection _libraries) { + if (_libraries == null) + _libraries = Collections.emptyList(); + final Collection libraries = _libraries; + loaderExecutor.execute(new Runnable() { + @Override + public void run() { + // Increment loadCount to signal that a new load cycle is on the way. + Integer loadId = loadCount.incrementAndGet(); + try { + loaderSemaphore.acquire(); + beginPopulate(loadId); + } catch (InterruptedException e) { + ExceptionUtils.logError(e); + } catch (RuntimeException e) { + loaderSemaphore.release(); + ExceptionUtils.logAndShowError(e); + } catch (Error e) { + loaderSemaphore.release(); + ExceptionUtils.logAndShowError(e); + } + } + + void beginPopulate(Integer loadId) { + synchronized (groups) { + // Must use toArray since groups are removed within the loop + for (Iterator> it = groups.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = it.next(); + if (!libraries.contains(entry.getKey())) { + PGroup group = entry.getValue(); + it.remove(); + groupViewers.remove(entry.getKey()); + if (group != null && !group.isDisposed()) + ThreadUtils.asyncExec(swtThread, disposer(group)); + } + } + Set groupDescs = new TreeSet(groupComparator); + for (ISymbolGroup lib : libraries) { + PGroup group = groups.get(lib); + //String label = group != null ? group.getText() : lib.getName(); + String label = lib.getName(); + groupDescs.add(new GroupDescriptor(lib, label, group)); + } + + // Populate all the missing groups. + IFilter groupFilter = currentGroupFilter; + populateGroups( + loaderExecutor, + null, + groupDescs.iterator(), + groupFilter, + loadId, + new Runnable() { + @Override + public void run() { + loaderSemaphore.release(); + } + }); + } + } + }); + } + + void populateGroups( + final ExecutorService exec, + final Control lastGroup, + final Iterator iter, + final IFilter groupFilter, + final Integer loadId, + final Runnable loadComplete) + { + // Check whether to still continue this population or not. + int currentLoadId = loadCount.get(); + if (currentLoadId != loadId) { + loadComplete.run(); + return; + } + + if (!iter.hasNext()) { + ThreadUtils.asyncExec(swtThread, new Runnable() { + @Override + public void run() { + if (filter.isDisposed() || c.isDisposed()) + return; + filter.focus(); + c.setVisible(true); + } + }); + loadComplete.run(); + return; + } + + final GroupDescriptor desc = iter.next(); + + ThreadUtils.asyncExec(swtThread, new Runnable() { + @Override + public void run() { + // Must make sure that loadComplete is invoked under error + // circumstances. + try { + populateGroup(); + } catch (RuntimeException e) { + loadComplete.run(); + ExceptionUtils.logAndShowError(e); + } catch (Error e) { + loadComplete.run(); + ExceptionUtils.logAndShowError(e); + } + } + + public void populateGroup() { + if (c.isDisposed()) { + loaderSemaphore.release(); + return; + } + // $ SWT-begin + //System.out.println("populating: " + desc.label); + PGroup group = desc.group; + 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); + } + + groups.put(desc.lib, group); + + group.setData(SymbolLibraryKeys.KEY_GROUP, desc.lib); + group.setData(KEY_USER_EXPANDED, defaultExpanded); + + group.setExpanded(defaultExpanded); + 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); + } + } + + group.setText(desc.label); + group.setToolTipText(desc.label); + + // Initialize the group: set content provider, label provider and input. + // NOTE: this should not yet start loading any data, just setup the viewer. + // [Tuukka @ 2009-10-24] changed group contents to be + // initialized lazily when needed. See references to initializeGroup(PGroup). + //initializeGroup(group); + + // 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; + exec.execute(new Runnable() { + @Override + public void run() { + populateGroups(exec, group_, iter, groupFilter, loadId, loadComplete); + } + }); + } + }); + } + + /** + * @param group + * @return null if GalleryViewer is currently being created + */ + GalleryViewer initializeGroup(final PGroup group) { + if (group.isDisposed()) + return null; + + //System.out.println("initializeGroup(" + group + ")"); + + 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 wait until the SWT/AWT UI population has completed + // and it will dispatch SWT events while waiting for AWT thread population + // to complete. This may in turn cause other parties to invoke this same + // initializeGroup method with the same group as parameter. This case + // needs to be handled appropriately by callers. + GalleryViewer viewer = new GalleryViewer(group); + + ISymbolGroup input = (ISymbolGroup) group.getData(SymbolLibraryKeys.KEY_GROUP); + initializeViewer(group, input, viewer); + + groupViewers.put(input, viewer); + group.setData(SymbolLibraryKeys.KEY_GALLERY, viewer); + + //System.out.println("initialized group: " + group.getText()); + + return viewer; + } + + void initializeViewer(final PGroup group, final ISymbolGroup input, final GalleryViewer viewer) { + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.BEGINNING).grab(true, false).applyTo(viewer.getControl()); + viewer.addDragSupport(new DragSourceParticipant()); + viewer.setAlign(FlowLayout.Align.Left); + viewer.getDiagram().setHint(SynchronizationHints.ERROR_HANDLER, errorHandler); + + viewer.setContentProvider(new IStructuredContentProvider() { + + /** + * Returns the elements in the input, which must be either an array or a + * Collection. + */ + @Override + public Object[] getElements(Object inputElement) { + if(inputElement == null) return new Object[0]; + return ((ISymbolGroup)inputElement).getItems(); + } + + /** + * This implementation does nothing. + */ + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // do nothing. + } + + /** + * This implementation does nothing. + */ + @Override + public void dispose() { + // do nothing. + } + + }); + viewer.setLabelProvider(new LabelProvider()); + viewer.setInput(input); + + // Add event handler that closes libraries on double clicks into empty + // space in library. + viewer.getCanvasContext().getEventHandlerStack().add(new IEventHandler() { + @Override + public int getEventMask() { + return EventTypes.MouseDoubleClickMask; + } + @Override + public boolean handleEvent(org.simantics.scenegraph.g2d.events.Event e) { + if (e instanceof MouseDoubleClickedEvent) { + PickRequest req = new PickRequest(((MouseDoubleClickedEvent) e).controlPosition); + Collection result = new ArrayList(); + DiagramUtils.pick(viewer.getDiagram(), req, result); + if (!result.isEmpty()) + return false; + + //System.out.println("NOTHING CLICKED"); + if (group.isDisposed()) + return false; + group.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + 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) { + if (!filter.isEmpty()) { + // Force searching in lowercase. + filter = filter.toLowerCase(); + + // Construct a regular expression from the specified text. + String regExFilter = filter + .replace("\\", "\\\\") // \ -> \\ + .replace(".", "\\.") // . -> \. + .replace("*", ".*") // * -> Any 0..n characters + .replace("?", ".") // ? -> Any single character + .replace("+", "\\+") // + -> \+ + .replace("(", "\\(") // ( -> \( + .replace(")", "\\)") // ) -> \) + .replace("[", "\\[") // [ -> \[ + .replace("]", "\\]") // ] -> \] + .replace("{", "\\{") // { -> \{ + .replace("}", "\\}") // } -> \} + .replace("^", "\\^") // ^ -> \^ + .replace("$", "\\$") // $ -> \$ + .replace("|", ".*|") // $ -> \$ + //.replace("|", "\\|") // | -> \| + .replace("&&", "\\&&") // && -> \&& + ; + + if (!regExFilter.startsWith(".*")) + regExFilter = ".*" + regExFilter ; + if (!regExFilter.endsWith(".*")) + regExFilter += ".*" ; + + return regExFilter; + } + return null; + } + + static class SymbolItemFilter extends ViewerFilter { + private final String string; + private final Pattern pattern; + + public SymbolItemFilter(String string, Pattern pattern) { + this.string = string; + this.pattern = pattern; + } + + @Override + public boolean select(Viewer viewer, Object parentElement, Object element) { + ISymbolItem item = (ISymbolItem) element; + String name = item.getName(); + Matcher m = pattern.matcher(name.toLowerCase()); + //System.out.println(name + ": " + (m.matches() ? "PASS" : "FAIL")); + return m.matches(); + } + + @Override + public int hashCode() { + return string == null ? 0 : string.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SymbolItemFilter other = (SymbolItemFilter) obj; + if (string == null) { + if (other.string != null) + return false; + } else if (!string.equals(other.string)) + return false; + return true; + } + } + + static Pattern toPattern(String filterText) { + String regExFilter = toPatternString(filterText); + Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY; + return pattern; + } + + static IFilter composeFilter(final FilterConfiguration config) { + final Mode mode = config.getMode(); + final List patterns = new ArrayList(); + for (GroupFilter f : config.getFilters()) { + if (f.isActive()) + patterns.add(toPattern(f.getFilterText())); + } + return new IFilter() { + @Override + public boolean select(Object toTest) { + if (patterns.isEmpty()) + return true; + + String s = (String) toTest; + switch (mode) { + case AND: + for (Pattern pat : patterns) { + Matcher m = pat.matcher(s.toLowerCase()); + //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL")); + if (!m.matches()) + return false; + } + return true; + case OR: + for (Pattern pat : patterns) { + Matcher m = pat.matcher(s.toLowerCase()); + //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL")); + if (m.matches()) + return true; + } + return false; + default: + throw new Error("Shouldn't happen"); + } + } + }; + } + + void updateFilterConfiguration(FilterConfiguration config) { + this.config = config; + IFilter filter = composeFilter(config); + this.currentGroupFilter = filter; + } + + void applyGroupFilters() { + IFilter groupFilter = this.currentGroupFilter; + final boolean[] changed = new boolean[] { false }; + + Control[] grps = c.getChildren(); + for (Control ctrl : grps) { + final PGroup grp = (PGroup) ctrl; + boolean visible = grp.getVisible(); + boolean shouldBeVisible = groupFilter.select(grp.getText()); + boolean change = visible != shouldBeVisible; + changed[0] |= change; + + grp.setData(KEY_GROUP_FILTERED, Boolean.valueOf(!shouldBeVisible)); + if (change) { + setGroupVisible(grp, shouldBeVisible); + } + } + + ThreadUtils.asyncExec(swtThread, new Runnable() { + @Override + public void run() { + if (c.isDisposed()) + return; + if (changed[0]) { + c.layout(true); + syncRefreshScrolledComposite(); + } + } + }); + } + + /** + * Filters the symbol groups and makes them visible/invisible as necessary. + * Invoke only from the SWT thread. + * + * @param text the filter text given by the client + * @return true if all groups were successfully filtered + * without asynchronous results + */ + boolean filterGroups(String text) { + //System.out.println("FILTERING WITH TEXT: " + text); + + String regExFilter = toPatternString(text); + Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY; + + this.currentFilterPattern = pattern; + final boolean[] changed = new boolean[] { false }; + boolean filteringComplete = true; + + ViewerFilter filter = null; + if (regExFilter != null) + filter = new SymbolItemFilter(regExFilter, pattern); + + Control[] grps = c.getChildren(); + for (Control ctrl : grps) { + final PGroup grp = (PGroup) ctrl; + if (grp.isDisposed()) + continue; + Boolean contentsChanged = filterGroup(grp, filter); + if (contentsChanged == null) + filteringComplete = false; + else + changed[0] = contentsChanged; + } + + ThreadUtils.asyncExec(swtThread, new Runnable() { + @Override + public void run() { + if (c.isDisposed()) + return; + if (changed[0]) { + c.layout(true); + syncRefreshScrolledComposite(); + } + } + }); + + return filteringComplete; + } + + static boolean objectEquals(Object o1, Object o2) { + if (o1==o2) return true; + if (o1==null && o2==null) return true; + if (o1==null || o2==null) return false; + return o1.equals(o2); + } + + /** + * @param grp + * @return true if the filtering caused changes in the group, + * false if not, and null if filtering + * could not be performed yet, meaning results need to be asked + * later + */ + private Boolean filterGroup(PGroup grp, ViewerFilter filter) { + boolean changed = false; + GalleryViewer viewer = initializeGroup(grp); + if (viewer == null) + return null; + + ViewerFilter lastFilter = viewer.getFilter(); + + boolean groupFiltered = Boolean.TRUE.equals(grp.getData(KEY_GROUP_FILTERED)); + boolean userExpanded = Boolean.TRUE.equals(grp.getData(KEY_USER_EXPANDED)); + final boolean expanded = grp.getExpanded(); + final boolean visible = grp.getVisible(); + final boolean filterChanged = !objectEquals(filter, lastFilter); + + // Find out how much data would be shown with the new filter. + viewer.setFilter(filter); + Object[] elements = viewer.getFilteredElements(); + + boolean shouldBeVisible = !groupFiltered && elements.length > 0; + boolean shouldBeExpanded = shouldBeVisible && (filter != null || userExpanded); + +// System.out.format("%40s: visible/should be = %5s %5s, expanded/user expanded/should be = %5s %5s %5s\n", +// grp.getText(), +// String.valueOf(visible), +// String.valueOf(shouldBeVisible), +// String.valueOf(expanded), +// String.valueOf(userExpanded), +// String.valueOf(shouldBeExpanded)); + + if (filterChanged || visible != shouldBeVisible || expanded != shouldBeExpanded) { + changed = true; + + if (shouldBeVisible == userExpanded) { + if (expanded != shouldBeExpanded) + setGroupExpandedWithoutNotification(grp, shouldBeExpanded); + setGroupVisible(grp, shouldBeVisible); + } else { + if (filter != null) { + if (shouldBeVisible) { + // The user has not expanded this group but the group contains + // stuff that matches the non-empty filter => show the group. + setGroupExpandedWithoutNotification(grp, true); + setGroupVisible(grp, true); + } else { + // The user has expanded this group but it does not contain items + // should should be shown with the current non-empty filter => hide the group. + setGroupExpandedWithoutNotification(grp, true); + setGroupVisible(grp, false); + } + } else { + // All groups should be visible. Some should be expanded and others not. + if (expanded != userExpanded) + setGroupExpandedWithoutNotification(grp, userExpanded); + if (!visible) + setGroupVisible(grp, true); + } + } + + if (shouldBeExpanded) { + viewer.refreshWithContent(elements); + } + } + +// String label = grp.getText(); +// Matcher m = pattern.matcher(label.toLowerCase()); +// boolean visible = m.matches(); +// if (visible != grp.getVisible()) { +// changed = true; +// setGroupVisible(grp, visible); +// } + + return changed; + } + + void setGroupExpandedWithoutNotification(PGroup grp, boolean expanded) { + // Ok, don't need to remove/add expand listener, PGroup will not notify + // listeners when setExpanded is invoked. + //grp.removeExpandListener(groupExpandListener); + grp.setExpanded(expanded); + //grp.addExpandListener(groupExpandListener); + } + + void setGroupVisible(PGroup group, boolean visible) { + GridData gd = (GridData) group.getLayoutData(); + gd.exclude = !visible; + group.setVisible(visible); + } + + boolean isGroupFiltered(String label) { + return !currentFilterPattern.matcher(label.toLowerCase()).matches(); + } + + class DragSourceParticipant extends AbstractDiagramParticipant implements IDragSourceParticipant { + @Reference Selection selection; + @Dependency PointerInteractor pi; + @Dependency TransformUtil util; + @Dependency PickContext pickContext; + + @Override + public int canDrag(MouseDragBegin me) { + if (me.button != MouseEvent.LEFT_BUTTON) return 0; + if (getHint(Hints.KEY_TOOL) != Hints.POINTERTOOL) return 0; + assertDependencies(); + + PickRequest req = new PickRequest(me.startCanvasPos); + req.pickPolicy = PickRequest.PickPolicy.PICK_INTERSECTING_OBJECTS; + List picks = new ArrayList(); + pickContext.pick(diagram, req, picks); + Set sel = selection.getSelection(me.mouseId); + + if (Collections.disjoint(sel, picks)) return 0; + // Box Select + return DnDConstants.ACTION_LINK; + } + + @Override + public Transferable dragStart(DragGestureEvent e) { + AWTChassis chassis = (AWTChassis) e.getComponent(); + ICanvasContext cc = chassis.getCanvasContext(); + Selection sel = cc.getSingleItem(Selection.class); + + Set ss = sel.getSelection(0); + if (ss.isEmpty()) return null; + Object[] res = new Object[ss.size()]; + int index = 0; + for (IElement ee : ss) + res[index++] = ee.getHint(ElementHints.KEY_OBJECT); + + ISelection object = new StructuredSelection(res); + + return new LocalObjectTransferable(object); + } + + @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); + //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); + //System.out.println("item expanded: " + group + ", " + sc.getClientArea()); + final GalleryViewer viewer = initializeGroup(group); + if (viewer == null) + return; + ThreadUtils.asyncExec(swtThread, new Runnable() { + @Override + public void run() { + viewer.refresh(); + refreshScrolledComposite(); + } + }); + } + }; + + @Override + public void setFocus() { + c.setFocus(); + } + + 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(); + } + }; + } + + void contributeActions() { + IToolBarManager toolbar = getViewSite().getActionBars().getToolBarManager(); + toolbar.add(new Action("Collapse All", + BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/collapseall.gif")) { + @Override + public void run() { + setAllExpandedStates(false); + } + }); + toolbar.add(new Action("Expand All", + BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/expandall.gif")) { + @Override + public void run() { + setAllExpandedStates(true); + } + }); + toolbar.add(new Action("Configure Filters", + BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/filter_ps.gif")) { + @Override + public void run() { + FilterConfiguration clone = new FilterConfiguration(config); + FilterDialog fd = new FilterDialog(getViewSite().getShell(), Activator.getDefault().getDialogSettings(), clone); + int result = fd.open(); + if (result != Window.OK) + return; + + updateFilterConfiguration(clone); + applyGroupFilters(); + + try { + savePreferences(); + } catch (BackingStoreException e) { + ExceptionUtils.logAndShowError(e); + } + } + }); + } + + void setAllExpandedStates(boolean targetState) { + Boolean targetStateObj = Boolean.valueOf(targetState); + boolean changed = false; + setDefaultExpanded(targetState); + Control[] grps = c.getChildren(); + //for (final PGroup grp : groups.values().toArray(new PGroup[0])) { + for (Control control : grps) { + final PGroup grp = (PGroup) control; + grp.setData(KEY_USER_EXPANDED, targetStateObj); + if (!grp.isDisposed() && grp.getExpanded() != targetState && grp.getVisible()) { + final GalleryViewer viewer = initializeGroup(grp); + setGroupExpandedWithoutNotification(grp, targetState); + ThreadUtils.asyncExec(swtThread, new Runnable() { + @Override + public void run() { + if (!grp.isDisposed()) { + if (viewer != null) + viewer.refresh(); + refreshScrolledComposite(); + } + } + }); + changed = true; + } + } + if (changed) + refreshScrolledComposite(); + } + + 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() { + ThreadUtils.getBlockingWorkExecutor().execute(new Runnable() { + @Override + public void run() { + runBlocking(); + } + }); + } + + private void runBlocking() { + try { + IHintObservable hints = null; + 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; + } + + hints = viewer.getDiagram(); + if (hints == null) + throw new ProvisionException("No diagram available for GalleryViewer of group " + group); + + ElementClass ec = item.getElementClass(hints); + StaticSymbol ss = ec.getSingleItem(StaticSymbol.class); + Image source = ss == null ? DefaultImages.UNKNOWN2.get() : ss.getImage(); + imageProxy.setSource(source); + } 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 { + } + } + } + + public FilterArea getFilterArea() { + return filter; + } + + 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) { + GalleryViewer viewer = groupViewers.get(group); + if (viewer != null) { + ISymbolItem[] input = group.getItems(); + viewer.setInput(input); + } + } + }; + + @Override + @SuppressWarnings("rawtypes") + public Object getAdapter(Class adapter) { + // For supporting Scene Graph viewer + if (adapter == INode[].class) { + List result = new ArrayList(groupViewers.size()); + for (GalleryViewer viewer : groupViewers.values()) { + result.add(viewer.getCanvasContext().getSceneGraph()); + } + return result.toArray(new INode[result.size()]); + } + return super.getAdapter(adapter); + } + +}