/*******************************************************************************
* Copyright (c) 2001, 2016 IBM Corporation and others.
* 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:
* IBM Corporation - initial API and implementation
* Obeo - Contribution in the project EEF
*******************************************************************************/
package org.eclipse.eef.properties.ui.api;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.eef.common.ui.api.EEFWidgetFactory;
import org.eclipse.eef.common.ui.api.IEEFFormContainer;
import org.eclipse.eef.properties.ui.internal.EEFTabbedPropertyViewPlugin;
import org.eclipse.eef.properties.ui.internal.page.EEFMessagePrefixProvider;
import org.eclipse.eef.properties.ui.internal.page.EEFPartListenerAdapter;
import org.eclipse.eef.properties.ui.internal.page.EEFTabbedPropertyComposite;
import org.eclipse.eef.properties.ui.internal.page.EEFTabbedPropertyViewer;
import org.eclipse.eef.properties.ui.internal.page.EEFTabbedPropertyViewer.IEEFTabDescriptorChangedListener;
import org.eclipse.eef.properties.ui.internal.registry.EEFTabbedPropertyRegistry;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.forms.widgets.Form;
import org.eclipse.ui.part.IContributedContentsView;
import org.eclipse.ui.part.Page;
import org.eclipse.ui.views.properties.IPropertySheetPage;
import org.eclipse.ui.views.properties.PropertySheet;
/**
* A property sheet page that provides a tabbed UI.
*
* @author Anthony Hunter
* @author Stephane Begaudeau
* @since 1.6.0
*/
public class EEFTabbedPropertySheetPage extends Page implements IPropertySheetPage, IEEFFormContainer {
/**
* The widget factory.
*/
private EEFWidgetFactory widgetFactory;
/**
* The composite holding all the widgets.
*/
private EEFTabbedPropertyComposite tabbedPropertyComposite;
/**
* The viewer.
*/
private EEFTabbedPropertyViewer tabbedPropertyViewer;
/**
* The workbench part requesting the use of the property page.
*/
private IEEFTabbedPropertySheetPageContributor contributor;
/**
* The currently active tab.
*/
private EEFTabContents currentTab;
/**
* The map of the tab descriptor to their instantiated tab.
*/
private Map<IEEFTabDescriptor, EEFTabContents> descriptorToTab = new HashMap<IEEFTabDescriptor, EEFTabContents>();
/**
* The map of the tab to their main composite.
*/
private Map<EEFTabContents, Composite> tabToComposite = new HashMap<EEFTabContents, Composite>();
/**
* This queue will contain the list of the label of the selected tabs.
*/
private List<String> selectionQueue = new ArrayList<String>();
/**
* Indicates that the selection queue is locked by the user.
*/
private boolean selectionQueueLocked;
/**
* The tab selection listener.
*/
private List<IEEFTabSelectionListener> tabSelectionListeners = new ArrayList<IEEFTabSelectionListener>();
/**
* The workbench window.
*/
private IWorkbenchWindow cachedWorkbenchWindow;
/**
* The part activation listener used to manage a part of the lifecycle of the property sheet page.
*/
private IPartListener partActivationListener;
/**
* Indicates if we should activate the property sheet.
*/
private boolean activePropertySheet;
/**
* The current part.
*/
private IWorkbenchPart currentPart;
/**
* The current selection.
*/
private ISelection currentSelection;
/**
* The form used to contain the all the widgets.
*/
private Form form;
/**
* Boolean flag indicating we are inside the rendering/refresh pahse of the page's lifecycle.
*/
private AtomicBoolean isRenderingInProgress = new AtomicBoolean(false);
/**
* The widget listener used to resize the scrolled composite.
*/
private ControlAdapter scrolledCompositeListener;
/**
* The listener used to forward tab selection changes.
*/
private IEEFTabDescriptorChangedListener viewerSelectionListener;
/**
* The constructor.
*
* @param contributor
* the contributor.
*/
public EEFTabbedPropertySheetPage(IEEFTabbedPropertySheetPageContributor contributor) {
this.contributor = contributor;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.ui.part.IPage#createControl(org.eclipse.swt.widgets.Composite)
*/
@Override
public void createControl(Composite parent) {
this.widgetFactory = new EEFWidgetFactory();
this.form = this.widgetFactory.createForm(parent);
this.form.setText(""); //$NON-NLS-1$
this.widgetFactory.decorateFormHeading(form);
this.form.getMessageManager().setMessagePrefixProvider(new EEFMessagePrefixProvider());
this.form.getMessageManager().setDecorationPosition(SWT.LEFT | SWT.TOP);
this.form.getMessageManager().setAutoUpdate(false);
form.getBody().setLayout(new FormLayout());
this.tabbedPropertyComposite = new EEFTabbedPropertyComposite(form.getBody(), this.widgetFactory);
this.widgetFactory.paintBordersFor(this.tabbedPropertyComposite);
this.tabbedPropertyComposite.setLayout(new FormLayout());
FormData formData = new FormData();
formData.left = new FormAttachment(0, 0);
formData.right = new FormAttachment(100, 0);
formData.top = new FormAttachment(0, 0);
formData.bottom = new FormAttachment(100, 0);
this.tabbedPropertyComposite.setLayoutData(formData);
this.form.setLayoutData(formData);
this.widgetFactory.paintBordersFor(form);
this.tabbedPropertyViewer = new EEFTabbedPropertyViewer(this.tabbedPropertyComposite.getTabbedPropertyList());
this.viewerSelectionListener = new IEEFTabDescriptorChangedListener() {
@Override
public void selectionChanged(IEEFTabDescriptor descriptor) {
EEFTabbedPropertySheetPage.this.processSelectionChanged(descriptor);
}
};
this.tabbedPropertyViewer.addSelectionListener(viewerSelectionListener);
this.scrolledCompositeListener = new ControlAdapter() {
@Override
public void controlResized(ControlEvent e) {
EEFTabbedPropertySheetPage.this.resizeScrolledComposite();
}
};
this.tabbedPropertyComposite.getScrolledComposite().addControlListener(scrolledCompositeListener);
this.partActivationListener = new EEFPartListenerAdapter() {
@Override
public void partOpened(IWorkbenchPart part) {
EEFTabbedPropertySheetPage.this.handlePartActivated(part);
}
};
this.cachedWorkbenchWindow = this.getSite().getWorkbenchWindow();
this.cachedWorkbenchWindow.getPartService().addPartListener(this.partActivationListener);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.ui.ISelectionListener#selectionChanged(org.eclipse.ui.IWorkbenchPart,
* org.eclipse.jface.viewers.ISelection)
*/
@Override
public void selectionChanged(IWorkbenchPart part, ISelection selection) {
EEFTabbedPropertyViewPlugin.getPlugin().debug("EEFTabbedPropertySheetPage#selectionChanged(...)"); //$NON-NLS-1$
this.setInput(part, selection);
}
/**
* Set the input of the page.
*
* @param part
* The current workbench part
* @param selection
* The current selection
*/
private void setInput(IWorkbenchPart part, ISelection selection) {
EEFTabbedPropertyViewPlugin.getPlugin().debug("EEFTabbedPropertySheetPage#setInput()"); //$NON-NLS-1$
if (selection == null || selection.equals(currentSelection)) {
return;
}
doSetInput(part, selection);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.eef.common.ui.api.IEEFFormContainer#refreshPage()
*/
@Override
public void refreshPage() {
Display display = this.getSite().getShell().getDisplay();
if (display != null) {
display.asyncExec(new Runnable() {
@Override
public void run() {
doSetInput(currentPart, currentSelection);
}
});
}
}
/**
* Indicates whether or not we are inside the rendering/refresh pahse of the page's lifecycle.
*
* @return <code>true</code> if we are inside the rendering/refresh pahse of the page's lifecycle.
*/
@Override
public boolean isRenderingInProgress() {
return isRenderingInProgress.get();
}
/**
* Set or reset the input of the page.
*
* @param part
* The current workbench part
* @param selection
* The current selection
*/
private synchronized void doSetInput(IWorkbenchPart part, ISelection selection) {
if (this.tabbedPropertyComposite.isDisposed()) {
return;
}
isRenderingInProgress.set(true);
this.tabbedPropertyComposite.setRedraw(false);
try {
this.currentPart = part;
this.currentSelection = selection;
this.contributor.updateFormTitle(this.form, this.currentSelection);
// see if the selection provides a new contributor
// validateRegistry(selection);
List<IEEFTabDescriptor> descriptors = EEFTabbedPropertyRegistry.getDefault(contributor).getTabDescriptors(part, selection);
this.updateTabs(descriptors);
// update tabs list
this.tabbedPropertyViewer.setInput(descriptors);
int lastTabSelectionIndex = this.getLastTabSelection(descriptors);
IEEFTabDescriptor selectedTab = this.tabbedPropertyViewer.getTabDescriptionAtIndex(lastTabSelectionIndex);
this.selectionQueueLocked = true;
try {
if (selectedTab == null) {
this.tabbedPropertyViewer.setSelectedTabDescriptor(null);
} else {
this.tabbedPropertyViewer.setSelectedTabDescriptor(selectedTab);
}
} finally {
this.selectionQueueLocked = false;
}
} finally {
this.tabbedPropertyComposite.setRedraw(true);
isRenderingInProgress.set(false);
}
}
/**
* Update the current tabs to represent the given input object. When tabs apply for both the old and new input they
* are reused otherwise they are disposed. If the current visible tab will not be reused (i.e. will be disposed) we
* have to send it an aboutToBeHidden() message.
*
* @param descriptors
* The tab descriptors.
*/
protected void updateTabs(List<IEEFTabDescriptor> descriptors) {
Map<IEEFTabDescriptor, EEFTabContents> newTabs = new HashMap<IEEFTabDescriptor, EEFTabContents>(descriptors.size() * 2);
boolean disposingCurrentTab = this.currentTab != null;
for (IEEFTabDescriptor descriptor : descriptors) {
EEFTabContents tab = this.descriptorToTab.remove(descriptor);
if (tab != null && tab.controlsHaveBeenCreated()) {
if (tab == this.currentTab) {
disposingCurrentTab = false;
}
} else {
tab = descriptor.createTab();
}
newTabs.put(descriptor, tab);
}
if (disposingCurrentTab) {
/**
* If the current tab is about to be disposed we have to call aboutToBeHidden
*/
this.currentTab.aboutToBeHidden();
this.currentTab = null;
}
this.disposeTabs(this.descriptorToTab.values());
this.descriptorToTab = newTabs;
}
/**
* Returns the last known selected tab for the given input.
*
* @param descriptors
* The tab descriptors
* @return The index of the currently selected tab (0, the first tab, by default)
*/
private int getLastTabSelection(List<IEEFTabDescriptor> descriptors) {
if (descriptors.size() != 0) {
for (String text : this.selectionQueue) {
int i = 0;
for (IEEFTabDescriptor descriptor : descriptors) {
if (text.equals(descriptor.getLabel())) {
return i;
}
i = i + 1;
}
}
}
return 0;
}
/**
* Disposes the TabContents objects passed to this method. If the 'currentTab' is going to be disposed, then the
* caller should call aboutToBeHidden() on the currentTab and set it to null before calling this method. Also, the
* caller needs to ensure that descriptorToTab map entries corresponding to the disposed TabContents objects are
* also removed.
*
* @param tabs
* The list of tabs
*/
protected void disposeTabs(Collection<EEFTabContents> tabs) {
for (EEFTabContents tab : tabs) {
Composite composite = tabToComposite.remove(tab);
tab.dispose();
if (composite != null) {
composite.dispose();
}
}
}
/**
* Handle the part activated event.
*
* @param part
* the new activated part.
*/
private void handlePartActivated(IWorkbenchPart part) {
EEFTabbedPropertyViewPlugin.getPlugin().debug("EEFTabbedPropertySheetPage#partActivated(...)"); //$NON-NLS-1$
/*
* The properties view has been activated and the current page is this instance of TabbedPropertySheetPage
*/
boolean thisActivated = part instanceof PropertySheet && ((PropertySheet) part).getCurrentPage() == this;
/*
* When the active part changes and the part does not provide a selection that affects this property sheet page,
* the PropertySheet does not send us a selectionChanged() event. We need to be informed of these events since
* we want to send aboutToBeHidden() and aboutToBeShown() when the property sheet is hidden or shown.
*/
IContributedContentsView view = null;
if (!thisActivated && !matchesContributor(part) && !part.getSite().getId().equals(contributor.getContributorId())) {
/*
* Is the part is a IContributedContentsView for the contributor, for example, outline view.
*/
// Used to keep the compatibility with luna
Object object = part.getAdapter(IContributedContentsView.class);
view = (IContributedContentsView) object;
}
if (view == null || (view.getContributingPart() != null && !view.getContributingPart().equals(contributor))) {
if (this.activePropertySheet) {
if (currentTab != null) {
currentTab.aboutToBeHidden();
}
this.activePropertySheet = false;
}
return;
}
if (!this.activePropertySheet && currentTab != null) {
currentTab.aboutToBeShown();
currentTab.refresh();
}
this.activePropertySheet = true;
}
/**
* Tests whether the specified part is the same as the contributor we represent.
*
* @param part
* a workbench part.
* @return <code>true</code> if the specified part is the same as the contributor we represent.
*/
private boolean matchesContributor(IWorkbenchPart part) {
if (contributor instanceof AbstractEEFTabbedPropertySheetPageContributorWrapper) {
AbstractEEFTabbedPropertySheetPageContributorWrapper wrapper = (AbstractEEFTabbedPropertySheetPageContributorWrapper) contributor;
return part.equals(wrapper.getRealContributor());
} else {
return part.equals(contributor);
}
}
/**
* Handle the newly selected descriptor.
*
* @param descriptor
* The tab descriptor
*/
private synchronized void processSelectionChanged(IEEFTabDescriptor descriptor) {
isRenderingInProgress.set(true);
try {
EEFTabContents tab = null;
if (descriptor == null) {
EEFTabbedPropertyViewPlugin.getPlugin().debug("EEFTabbedPropertySheetPage -- Hide tab"); //$NON-NLS-1$
// pretend the tab is empty.
this.hideTab(this.currentTab);
} else {
// create tab if necessary
// can not cache based on the id - tabs may have the same id,
// but different section depending on the selection
tab = this.descriptorToTab.get(descriptor);
if (tab == null) {
// Fallback to a full search in case the descriptor has changed hashCode, which happens if the
// underlying EObject has changed URL (e.g. an EClass whose named has changed).
for (Map.Entry<IEEFTabDescriptor, EEFTabContents> entry : this.descriptorToTab.entrySet()) {
if (entry.getKey() == descriptor) {
tab = entry.getValue();
break;
}
}
}
if (tab != null) {
if (tab != this.currentTab) {
this.hideTab(this.currentTab);
}
Composite tabComposite = this.tabToComposite.get(tab);
if (tabComposite == null) {
tabComposite = this.createTabComposite();
tab.createControls(tabComposite, this);
// tabAreaComposite.layout(true);
this.tabToComposite.put(tab, tabComposite);
}
// force widgets to be resized
tab.setInput(this.currentPart, this.currentSelection);
// store tab selection
this.storeCurrentTabSelection(descriptor.getLabel());
if (tab != this.currentTab) {
this.showTab(tab);
}
tab.refresh();
}
}
tabbedPropertyComposite.getTabComposite().layout(true);
this.currentTab = tab;
this.resizeScrolledComposite();
if (descriptor != null) {
this.handleTabSelection(descriptor);
}
} finally {
isRenderingInProgress.set(false);
}
}
/**
* Show the given tab.
*
* @param tab
* The tab to show
*/
private void showTab(EEFTabContents tab) {
if (tab != null) {
Composite tabComposite = tabToComposite.get(tab);
if (tabComposite != null) {
/**
* the following method call order is important - do not change it or the widgets might be drawn
* incorrectly
*/
tabComposite.moveAbove(null);
tab.aboutToBeShown();
tabComposite.setVisible(true);
}
}
}
/**
* Hide the given tab.
*
* @param tab
* The tab to hide
*/
private void hideTab(EEFTabContents tab) {
if (tab != null) {
Composite tabComposite = tabToComposite.get(tab);
if (tabComposite != null) {
tab.aboutToBeHidden();
tabComposite.setVisible(false);
}
}
}
/**
* Create the composite for the tab.
*
* @return The composite to use for the new tab
*/
private Composite createTabComposite() {
Composite result = this.widgetFactory.createComposite(tabbedPropertyComposite.getTabComposite(), SWT.NO_FOCUS);
result.setVisible(false);
result.setLayout(new FillLayout());
result.setBackground(this.widgetFactory.getColors().getBackground());
result.setForeground(this.widgetFactory.getColors().getForeground());
FormData data = new FormData();
data.top = new FormAttachment(0, 0);
data.bottom = new FormAttachment(100, 0);
data.left = new FormAttachment(0, 0);
data.right = new FormAttachment(100, 0);
result.setLayoutData(data);
return result;
}
/**
* Stores the current tab label in the selection queue. Tab labels are used to carry the tab context from one input
* object to another. The queue specifies the selection priority. So if the first tab in the queue is not available
* for the input we try the second tab and so on. If none of the tabs are available we default to the first tab
* available for the input.
*
* @param label
* The label of the currently selected tab.
*/
private void storeCurrentTabSelection(String label) {
if (!selectionQueueLocked) {
selectionQueue.remove(label);
selectionQueue.add(0, label);
}
}
/**
* Handle the tab selection.
*
* @param descriptor
* The tab descriptor
*/
private void handleTabSelection(IEEFTabDescriptor descriptor) {
if (selectionQueueLocked) {
/*
* don't send tab selection events for non user changes.
*/
return;
}
for (IEEFTabSelectionListener listener : this.tabSelectionListeners) {
listener.tabSelected(descriptor);
}
}
/**
* Add a tab selection listener.
*
* @param listener
* a tab selection listener.
*/
public void addTabSelectionListener(IEEFTabSelectionListener listener) {
tabSelectionListeners.add(listener);
}
/**
* Remove a tab selection listener.
*
* @param listener
* a tab selection listener.
*/
public void removeTabSelectionListener(IEEFTabSelectionListener listener) {
tabSelectionListeners.remove(listener);
}
/**
* Resize the scrolled composite.
*/
public void resizeScrolledComposite() {
Point currentTabSize = new Point(0, 0);
if (this.currentTab != null) {
Composite sizeReference = this.tabToComposite.get(this.currentTab);
if (sizeReference != null) {
currentTabSize = sizeReference.computeSize(SWT.DEFAULT, SWT.DEFAULT);
}
}
this.tabbedPropertyComposite.getScrolledComposite().setMinSize(currentTabSize);
ScrollBar verticalScrollBar = this.tabbedPropertyComposite.getScrolledComposite().getVerticalBar();
if (verticalScrollBar != null) {
Rectangle clientArea = this.tabbedPropertyComposite.getScrolledComposite().getClientArea();
int increment = clientArea.height - 5;
verticalScrollBar.setPageIncrement(increment);
}
ScrollBar horizontalScrollBar = this.tabbedPropertyComposite.getScrolledComposite().getHorizontalBar();
if (horizontalScrollBar != null) {
Rectangle clientArea = this.tabbedPropertyComposite.getScrolledComposite().getClientArea();
int increment = clientArea.width - 5;
horizontalScrollBar.setPageIncrement(increment);
}
}
/**
* Set the currently selected tab to be that of the provided tab id.
*
* @param id
* The string id of the tab to select.
*/
public void setSelectedTab(String id) {
List<IEEFTabDescriptor> elements = this.tabbedPropertyViewer.getElements();
for (IEEFTabDescriptor descriptor : elements) {
if (descriptor.getId() != null && descriptor.getId().equals(id)) {
this.tabbedPropertyViewer.setSelectedTabDescriptor(descriptor);
}
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.ui.part.IPage#dispose()
*/
@Override
public void dispose() {
this.disposeContributor();
this.widgetFactory.dispose();
this.cachedWorkbenchWindow.getPartService().removePartListener(this.partActivationListener);
}
/**
* Dispose the contributor with the provided contributor id. This happens on part close as well as when contributors
* switch between the workbench part and contributor from a selection.
*/
private void disposeContributor() {
if (this.currentTab != null) {
this.currentTab.aboutToBeHidden();
}
Collection<EEFTabContents> tabs = this.descriptorToTab.values();
for (EEFTabContents tab : tabs) {
Composite composite = this.tabToComposite.remove(tab);
tab.dispose();
if (composite != null) {
composite.dispose();
}
}
this.descriptorToTab = new HashMap<IEEFTabDescriptor, EEFTabContents>();
}
/**
* Returns the list of currently active tabs.
*
* @return the currently active tabs.
*/
public List<IEEFTabDescriptor> getActiveTabs() {
return this.tabbedPropertyViewer.getElements();
}
/**
* {@inheritDoc}
*
* @see org.eclipse.ui.part.IPage#getControl()
*/
@Override
public Control getControl() {
return this.form;
}
/**
* Return the currentTab.
*
* @return the currentTab
*/
public EEFTabContents getCurrentTab() {
return this.currentTab;
}
/**
* Returns the currently selected tab.
*
* @return the currently selected tab or <code>null</code> if there is no tab selected.
*/
public IEEFTabDescriptor getSelectedTab() {
int selectedTab = tabbedPropertyViewer.getSelectionIndex();
if (selectedTab != -1) {
return tabbedPropertyViewer.getTabDescriptionAtIndex(selectedTab);
}
return null;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.ui.part.IPage#setActionBars(org.eclipse.ui.IActionBars)
*/
@Override
public void setActionBars(IActionBars actionBars) {
// do nothing for now
}
/**
* {@inheritDoc}
*
* @see org.eclipse.ui.part.IPage#setFocus()
*/
@Override
public void setFocus() {
this.getControl().setFocus();
}
/**
* Return the widgetFactory.
*
* @return the widgetFactory
*/
@Override
public EEFWidgetFactory getWidgetFactory() {
return this.widgetFactory;
}
/**
* Refresh the currently active tab.
*/
public void refresh() {
this.currentTab.refresh();
}
/**
* Return the contributor.
*
* @return the contributor
*/
public IEEFTabbedPropertySheetPageContributor getContributor() {
return this.contributor;
}
/**
* Return the form.
*
* @return the form
*/
@Override
public Form getForm() {
return this.form;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.eef.common.ui.api.IEEFFormContainer#getShell()
*/
@Override
public Shell getShell() {
return this.getSite().getShell();
}
}