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