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