/******************************************************************************* * 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); } }