--- /dev/null
+/*******************************************************************************\r
+ * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
+ * in Industry THTH ry.\r
+ * All rights reserved. This program and the accompanying materials\r
+ * are made available under the terms of the Eclipse Public License v1.0\r
+ * which accompanies this distribution, and is available at\r
+ * http://www.eclipse.org/legal/epl-v10.html\r
+ *\r
+ * Contributors:\r
+ * VTT Technical Research Centre of Finland - initial API and implementation\r
+ *******************************************************************************/\r
+package org.simantics.selectionview;\r
+\r
+import java.util.ArrayList;\r
+import java.util.Collection;\r
+import java.util.Collections;\r
+import java.util.List;\r
+import java.util.TreeSet;\r
+import java.util.concurrent.CopyOnWriteArrayList;\r
+import java.util.concurrent.atomic.AtomicBoolean;\r
+import java.util.concurrent.atomic.AtomicInteger;\r
+import java.util.function.Consumer;\r
+\r
+import org.eclipse.jface.layout.GridDataFactory;\r
+import org.eclipse.jface.layout.GridLayoutFactory;\r
+import org.eclipse.jface.resource.JFaceResources;\r
+import org.eclipse.jface.resource.LocalResourceManager;\r
+import org.eclipse.jface.viewers.ISelection;\r
+import org.eclipse.jface.viewers.ISelectionChangedListener;\r
+import org.eclipse.jface.viewers.ISelectionProvider;\r
+import org.eclipse.jface.viewers.SelectionChangedEvent;\r
+import org.eclipse.jface.viewers.StructuredSelection;\r
+import org.eclipse.swt.SWT;\r
+import org.eclipse.swt.layout.FillLayout;\r
+import org.eclipse.swt.widgets.Composite;\r
+import org.eclipse.swt.widgets.Control;\r
+import org.eclipse.swt.widgets.Event;\r
+import org.eclipse.swt.widgets.Listener;\r
+import org.eclipse.ui.IWorkbenchPartSite;\r
+import org.eclipse.ui.part.IPageSite;\r
+import org.simantics.browsing.ui.common.ErrorLogger;\r
+import org.simantics.browsing.ui.swt.TabbedPropertyPage;\r
+import org.simantics.db.ReadGraph;\r
+import org.simantics.db.common.request.ReadRequest;\r
+import org.simantics.db.common.request.UniqueRead;\r
+import org.simantics.db.exception.DatabaseException;\r
+import org.simantics.db.management.ISessionContext;\r
+import org.simantics.ui.SimanticsUI;\r
+import org.simantics.utils.ObjectUtils;\r
+import org.simantics.utils.datastructures.MapList;\r
+import org.simantics.utils.ui.ISelectionUtils;\r
+import org.simantics.utils.ui.jface.BasePostSelectionProvider;\r
+\r
+/**\r
+ * <p>\r
+ * Subclasses may extend or reimplement the following methods as required:\r
+ * <ul>\r
+ * <li><code>createBaseComposite</code> - reimplement to customize how the\r
+ * composite is constructed that is the basis of the whole tab container.</\r
+ * <li>\r
+ * </ul>\r
+ * </p>\r
+ * \r
+ * @author Tuukka Lehtonen\r
+ */\r
+public class TabbedPropertyTable extends Composite implements IPropertyTab {\r
+\r
+ public static final SelectionProcessor<Object, Object> DEFAULT_SELECTION_PROCESSOR = new SelectionProcessor<Object, Object>() {\r
+ @Override\r
+ public Collection<?> process(Object selection, Object object) {\r
+ return Collections.emptyList();\r
+ }\r
+ };\r
+\r
+ @SuppressWarnings("rawtypes")\r
+ private SelectionProcessor selectionProcessor = DEFAULT_SELECTION_PROCESSOR;\r
+\r
+ private Composite baseComposite;\r
+\r
+ private final List<IPropertyTab> tabs = new CopyOnWriteArrayList<IPropertyTab>();\r
+\r
+ private final AtomicInteger activeTab = new AtomicInteger(-1);\r
+\r
+ protected IWorkbenchPartSite sourceSite;\r
+\r
+ protected IPageSite pageSite;\r
+\r
+ protected ISelection currentSelection;\r
+\r
+ /**\r
+ * The selection provider set for the page site.\r
+ */\r
+ protected BasePostSelectionProvider pageSelectionProvider = new BasePostSelectionProvider() {\r
+\r
+ /**\r
+ * For preventing infinite recursion, not that it should happen. \r
+ */\r
+ private AtomicBoolean settingSelection = new AtomicBoolean();\r
+\r
+ /**\r
+ * Overridden like this because pageSelectionProvider is published to\r
+ * the workbench as the page site selection provider and therefore it\r
+ * possible that its {@link ISelectionProvider#setSelection(ISelection)}\r
+ * method is invoked externally and we need to propagate the selection\r
+ * to the underlying active tab and its selection provider instead of\r
+ * setting pageSelectionProvider's selection to anything.\r
+ */\r
+ @Override\r
+ public void setSelection(ISelection selection) {\r
+ if (settingSelection.compareAndSet(false, true)) {\r
+ IPropertyTab table = getActiveTab();\r
+ if (table != null && table.getSelectionProvider() != null)\r
+ table.getSelectionProvider().setSelection(selection);\r
+ settingSelection.set(false);\r
+ } else {\r
+ ErrorLogger.defaultLogWarning("Possible BUG: prevented recursive attempt to set selection for "\r
+ + TabbedPropertyTable.this.toString(), new Exception("trace"));\r
+ }\r
+ }\r
+ };\r
+\r
+// protected ISelectionChangedListener debugPageSelectionListener = new ISelectionChangedListener() {\r
+// {\r
+// pageSelectionProvider.addSelectionChangedListener(this);\r
+// }\r
+// @Override\r
+// public void selectionChanged(SelectionChangedEvent event) {\r
+// System.out.println("page selection change: " + event);\r
+// System.out.println(" provider: " + event.getSelectionProvider());\r
+// System.out.println(" selection: " + event.getSelection());\r
+// }\r
+// };\r
+\r
+ protected ISelectionChangedListener activeTabSelectionListener = new ISelectionChangedListener() {\r
+ @Override\r
+ public void selectionChanged(SelectionChangedEvent event) {\r
+// System.out.println("active tab selection change: " + event);\r
+// System.out.println(" provider: " + event.getSelectionProvider());\r
+// System.out.println(" selection: " + event.getSelection());\r
+ ISelection s = event.getSelection();\r
+ // This is a workaround to avert calling pageSelectionProvider.setSelection here.\r
+ pageSelectionProvider.setSelectionWithoutFiring(s);\r
+ pageSelectionProvider.fireSelection(s);\r
+ pageSelectionProvider.firePostSelection(s);\r
+ }\r
+ };\r
+\r
+ protected LocalResourceManager resourceManager;\r
+\r
+ public TabbedPropertyTable(IWorkbenchPartSite site, IPageSite pageSite, Composite parent, int style) {\r
+ super(parent, style);\r
+ if (site == null)\r
+ throw new IllegalArgumentException("null source site");\r
+ if (pageSite == null)\r
+ throw new IllegalArgumentException("null page site");\r
+ this.sourceSite = site;\r
+ this.pageSite = pageSite;\r
+ GridLayoutFactory.fillDefaults().applyTo(this);\r
+\r
+ resourceManager = new LocalResourceManager(JFaceResources.getResources(parent.getDisplay()));\r
+\r
+ addListener(SWT.Dispose, new Listener() {\r
+ @Override\r
+ public void handleEvent(Event event) {\r
+ //System.out.println("DISPOSING " + this + " " + System.identityHashCode(TabbedPropertyTable.this));\r
+ activeTab.set(-1);\r
+ tabs.clear();\r
+\r
+ currentSelection = null;\r
+ if (currentListener != null)\r
+ currentListener.dispose();\r
+\r
+ TabbedPropertyTable.this.pageSite = null;\r
+ TabbedPropertyTable.this.sourceSite = null;\r
+ resourceManager.dispose();\r
+ \r
+ }\r
+ });\r
+ }\r
+\r
+ @SuppressWarnings("rawtypes")\r
+ protected void setSelectionProcessor(SelectionProcessor selectionProcessor) {\r
+ this.selectionProcessor = selectionProcessor;\r
+ }\r
+\r
+ @Override\r
+ public void createControl(Composite parent, ISessionContext context) {\r
+ createBaseComposite(parent, null);\r
+ }\r
+\r
+ class InputListener implements org.simantics.db.procedure.Listener<Collection<?>> {\r
+\r
+ final private Consumer<Collection<?>> inputCallback;\r
+ private boolean disposed = false;\r
+\r
+ public InputListener(Consumer<Collection<?>> inputCallback) {\r
+ this.inputCallback = inputCallback;\r
+ }\r
+\r
+ @Override\r
+ public void exception(Throwable t) {\r
+ ErrorLogger.defaultLogError(t);\r
+ }\r
+\r
+ @Override\r
+ public void execute(Collection<?> result) {\r
+ inputCallback.accept(result);\r
+ }\r
+\r
+ @Override\r
+ public boolean isDisposed() {\r
+ return disposed || TabbedPropertyTable.this.isDisposed();\r
+ }\r
+\r
+ public void dispose() {\r
+ disposed = true;\r
+ }\r
+\r
+ }\r
+\r
+ InputListener currentListener = null;\r
+\r
+ /**\r
+ * Must be invoked from the SWT UI thread.\r
+ * \r
+ * @param selection the new selection\r
+ * @param force <code>true</code> to force the resetting of the new input\r
+ * even if it is the same as the previous one.\r
+ */\r
+ @Override\r
+ @SuppressWarnings("unchecked")\r
+ public void setInput(ISessionContext context, ISelection selection, boolean force) {\r
+ //System.out.println(hashCode() + "# TabbedPropertyTable.setInput(" + selection + ", " + force + ")");\r
+ if (isDisposed())\r
+ return;\r
+ if (context == null)\r
+ return;\r
+\r
+ // Check if this is a duplicate of the previous selection to reduce unnecessary flicker.\r
+ if (!force && ObjectUtils.objectEquals(currentSelection, selection))\r
+ return;\r
+\r
+// System.out.println("[3] setInput " + selection + ", force=" + force);\r
+ currentSelection = selection;\r
+\r
+ if (selectionProcessor != null) {\r
+ \r
+ if (currentListener != null) currentListener.dispose();\r
+ \r
+ final Collection<Object> contents = ISelectionUtils.convertSelection(selection);\r
+ if (contents.isEmpty())\r
+ return;\r
+\r
+ currentListener = new InputListener(inputCallback(contents, context));\r
+\r
+ // NOTE: must be an anonymous read to guarantee that each request\r
+ // will always be performed and not taken from DB caches.\r
+ context.getSession().asyncRequest(new ReadRequest() {\r
+ \r
+ @Override\r
+ public void run(ReadGraph graph) throws DatabaseException {\r
+ \r
+ graph.syncRequest(new UniqueRead<Collection<?>>() {\r
+ @Override\r
+ public Collection<?> perform(ReadGraph graph) throws DatabaseException {\r
+ //System.out.println("TabbedPropertyTable.setInput.perform(" + contents + ")");\r
+ return selectionProcessor.process(contents, graph);\r
+ }\r
+ }, currentListener);\r
+ \r
+ }\r
+ });\r
+ \r
+ }\r
+ }\r
+\r
+ protected Consumer<Collection<?>> inputCallback(final Collection<Object> selectionContents, final ISessionContext sessionContext) {\r
+ return new Consumer<Collection<?>>() {\r
+ @Override\r
+ public void accept(final Collection<?> contribs) {\r
+ \r
+ if (isDisposed())\r
+ return;\r
+// if (contribs.isEmpty())\r
+// return;\r
+\r
+ SimanticsUI.asyncExecSWT(TabbedPropertyTable.this, new Runnable() {\r
+ \r
+ public void run() {\r
+ \r
+ //System.out.println(Thread.currentThread() + " inputCallback: " + input);\r
+ //System.out.println("is TabbedPropertyTable " + this + " visible: " + isVisible());\r
+ if (!isVisible()) {\r
+ // Set current selection to null to force update when the\r
+ // page becomes visible\r
+ currentSelection = null;\r
+ return;\r
+ }\r
+\r
+ createBaseComposite(TabbedPropertyTable.this, new TabbedPropertyPage(sourceSite.getPart()) {\r
+\r
+ /**\r
+ * The selection providers for the current set of property tabs.\r
+ */\r
+ ISelectionProvider[] tabSelectionProviders = { null };\r
+\r
+ @Override\r
+ protected void initializePageSwitching() {\r
+ // Overridden to prevent TabbedPropertyPage\r
+ // from initializing PageSwitcher \r
+ }\r
+\r
+ @Override\r
+ protected int getContainerStyle() {\r
+ return TabbedPropertyTable.this.getTabFolderStyle();\r
+ }\r
+\r
+ @Override\r
+ protected void pageChange(int newPageIndex) {\r
+ int oldActiveTab = activeTab.getAndSet(newPageIndex);\r
+ //System.out.println(Thread.currentThread() + " page changed: from " + oldActiveTab + " to " + newPageIndex);\r
+ super.pageChange(newPageIndex);\r
+\r
+ // Remove possible old selection listeners from the hidden tab.\r
+ ISelection oldSelection = null;\r
+ IPropertyTab oldTab = null;\r
+ if (oldActiveTab > -1) {\r
+ oldTab = tabs.get(oldActiveTab);\r
+ ISelectionProvider pv = oldTab.getSelectionProvider();\r
+ if (pv != null) {\r
+ oldSelection = pv.getSelection();\r
+ pv.removeSelectionChangedListener(activeTabSelectionListener);\r
+ }\r
+ }\r
+\r
+ // Attach selection listeners to the activated tab if possible.\r
+ ISelection activeSelection = null;\r
+ IPropertyTab activeTab = getActiveTab();\r
+ if (activeTab != null) {\r
+ ISelectionProvider pv = activeTab.getSelectionProvider();\r
+ if (pv != null) {\r
+ activeSelection = pv.getSelection();\r
+ pv.addSelectionChangedListener(activeTabSelectionListener);\r
+ }\r
+ }\r
+\r
+ String oldLabel = null;\r
+ String newLabel = null;\r
+ if (oldActiveTab > -1)\r
+ oldLabel = getPageText(oldActiveTab);\r
+ if (newPageIndex > -1)\r
+ newLabel = getPageText(newPageIndex);\r
+ activeTabChanged(new TabChangeEvent(oldTab, oldLabel, activeTab, newLabel));\r
+\r
+ // This is a workaround to avert calling pageSelectionProvider.setSelection here.\r
+ pageSelectionProvider.setSelectionWithoutFiring(activeSelection);\r
+ if (!ObjectUtils.objectEquals(oldSelection, activeSelection)) {\r
+ pageSelectionProvider.fireSelection(activeSelection);\r
+ pageSelectionProvider.firePostSelection(activeSelection);\r
+ }\r
+ }\r
+\r
+ @Override\r
+ protected void createPages() {\r
+ \r
+ // 1. loop through a list of possible contributors\r
+ // 2. list the ones that can contribute, in priority order told by the contributions themselves\r
+ // 3. add pages in priority order\r
+ // 4. initialize pages\r
+ \r
+ // Categorize contributions by id\r
+ MapList<String,ComparableTabContributor> ml = new MapList<String,ComparableTabContributor>();\r
+ for (Object o : contribs) {\r
+ if (o instanceof ComparableTabContributor) {\r
+ ComparableTabContributor c = (ComparableTabContributor) o; \r
+ ml.add(c.getId(), c);\r
+ } else {\r
+ System.out.println("WARNING: SelectionProcessor produced an unusable contribution to TabbedPropertyTable: " + o);\r
+ }\r
+ }\r
+\r
+ // For each id take the greatest (id) contribution\r
+ Collection<ComparableTabContributor> contributions = new TreeSet<ComparableTabContributor>();\r
+ for(String key : ml.getKeys()) {\r
+ TreeSet<ComparableTabContributor> ts = new TreeSet<ComparableTabContributor>(ml.getValuesUnsafe(key));\r
+ ComparableTabContributor contrib = ts.first();\r
+ contributions.add(contrib);\r
+ }\r
+\r
+ // Sort contributions by id\r
+ List<ComparableTabContributor> contributionList = new ArrayList<ComparableTabContributor>(contributions);\r
+\r
+ if (contributions.isEmpty()) {\r
+ Composite empty = createEmptyControl(getContainer(), SWT.NONE);\r
+ addPage(empty, "No properties to show", null);\r
+ } else {\r
+ // Set single selection provider into pageSite\r
+ // that is dispatched every time the selection\r
+ // in the property view changes, be it because\r
+ // the selection changed in the current tab or\r
+ // because the active tab, i.e. the selection\r
+ // provider is changed.\r
+ pageSite.setSelectionProvider(pageSelectionProvider);\r
+\r
+ // Allocate space for each tab to specify\r
+ // its own selection provider.\r
+ tabSelectionProviders = new ISelectionProvider[contributions.size()];\r
+ int index = 0;\r
+\r
+ for (ComparableTabContributor cc : contributionList) {\r
+ Composite middleware = new Composite(getContainer(), SWT.NONE);\r
+ GridLayoutFactory.fillDefaults().applyTo(middleware);\r
+\r
+ Control control = null;\r
+\r
+ // Create a wrapper for pageSite to make it possible\r
+ // for each tab contribution to specify its own\r
+ // selection provider.\r
+ final int tabIndex = index++;\r
+ IPageSite siteWrapper = new PageSiteProxy(pageSite) {\r
+ @Override\r
+ public void setSelectionProvider(ISelectionProvider provider) {\r
+ tabSelectionProviders[tabIndex] = provider;\r
+ if(pageSelectionProvider != null && provider != null)\r
+ pageSelectionProvider.setAndFireNonEqualSelection(provider.getSelection());\r
+ }\r
+ @Override\r
+ public ISelectionProvider getSelectionProvider() {\r
+ return tabSelectionProviders[tabIndex];\r
+ }\r
+ };\r
+\r
+ IPropertyTab tab = cc.create(middleware, siteWrapper, sessionContext, cc.getInput());\r
+ if (tab != null) {\r
+ tabs.add(tab);\r
+ control = tab.getControl();\r
+ if(control != null && !control.isDisposed()) GridDataFactory.fillDefaults().grab(true, true).applyTo(control);\r
+ } else {\r
+ control = createEmptyControl(middleware, SWT.NONE);\r
+// Button b = new Button(middleware, SWT.BORDER);\r
+// GridDataFactory.fillDefaults().grab(true, true).applyTo(b);\r
+// b.setText("FOO");\r
+ }\r
+\r
+ addPage(middleware, cc.getLabel(), cc.getImage());\r
+\r
+ if (tab != null) {\r
+ Object input = cc.getInput();\r
+ tab.setInput(sessionContext, new StructuredSelection(input), false);\r
+ }\r
+ }\r
+\r
+ ComparableTabContributor cc = TabbedPropertyTable.this.selectInitialTab(selectionContents, contributionList);\r
+ if (cc != null) {\r
+ int activePage = contributionList.indexOf(cc);\r
+ if (activePage != -1)\r
+ setActivePage(activePage);\r
+ }\r
+ }\r
+ }\r
+\r
+ Composite createEmptyControl(Composite parent, int flags) {\r
+ Composite empty = new Composite(parent, flags);\r
+ GridLayoutFactory.fillDefaults().applyTo(empty);\r
+ return empty;\r
+ }\r
+ });\r
+ layout();\r
+ }\r
+ });\r
+ }\r
+ };\r
+ }\r
+\r
+ @Override\r
+ public Control getControl() {\r
+ return this;\r
+ }\r
+\r
+ @Override\r
+ public void requestFocus() {\r
+ IPropertyTab table = getActiveTab();\r
+ if (table != null)\r
+ table.requestFocus();\r
+ }\r
+\r
+ public IPropertyTab getActiveTab() {\r
+ int index = activeTab.get();\r
+ if (index < 0 || index >= tabs.size())\r
+ return null;\r
+ IPropertyTab tab = tabs.get(index);\r
+ return tab;\r
+ }\r
+\r
+ protected Composite createBaseComposite(final Composite parent, TabbedPropertyPage page) {\r
+ // 1. dispose the previous UI container\r
+ disposeAll();\r
+\r
+ // 2. construct new base for the tabbed property UI\r
+ baseComposite = new Composite(parent, SWT.NONE);\r
+\r
+ // <DEBUG>\r
+// parent.setBackground(getDisplay().getSystemColor(SWT.COLOR_RED));\r
+// baseComposite.setBackground(getDisplay().getSystemColor(SWT.COLOR_TITLE_INACTIVE_BACKGROUND));\r
+ // </DEBUG>\r
+\r
+ if (page != null)\r
+ page.createPartControl(baseComposite);\r
+\r
+ GridLayoutFactory.fillDefaults().applyTo(parent);\r
+ GridDataFactory.fillDefaults().grab(true, true).applyTo(baseComposite);\r
+ baseComposite.setLayout(new FillLayout());\r
+\r
+ return baseComposite;\r
+ }\r
+\r
+ private void disposeAll() {\r
+ if (baseComposite != null && !baseComposite.isDisposed()) {\r
+ baseComposite.dispose();\r
+ baseComposite = null;\r
+ }\r
+ activeTab.set(-1);\r
+ for (IPropertyTab tab : tabs)\r
+ tab.dispose();\r
+ tabs.clear();\r
+ }\r
+\r
+ @Override\r
+ public ISelectionProvider getSelectionProvider() {\r
+ return pageSelectionProvider;\r
+ }\r
+\r
+ /**\r
+ * Override to perform actions when the active tab changes.\r
+ *\r
+ * @param previousTab\r
+ * @param activeTab\r
+ */\r
+ protected void activeTabChanged(TabChangeEvent event) {\r
+ }\r
+\r
+ /**\r
+ * @param selectionContents \r
+ * @param contributions\r
+ * @return\r
+ */\r
+ protected ComparableTabContributor selectInitialTab(Collection<Object> selectionContents, Collection<ComparableTabContributor> contributions) {\r
+ return null;\r
+ }\r
+\r
+ /**\r
+ * @return\r
+ */\r
+ protected int getTabFolderStyle() {\r
+ return SWT.NONE;\r
+ }\r
+\r
+}\r