/*******************************************************************************
* 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
* Mariot Chauvin <mariot.chauvin@obeo.fr> - bug 259553
* Amit Joglekar <joglekar@us.ibm.com> - Support for dynamic images (bug 385795)
* Obeo - Contribution to the EEF project
*******************************************************************************/
package org.eclipse.eef.properties.ui.internal.page.propertylist;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.eef.common.ui.api.EEFWidgetFactory;
import org.eclipse.eef.properties.ui.api.IEEFTabItem;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.swt.SWT;
import org.eclipse.swt.accessibility.ACC;
import org.eclipse.swt.accessibility.Accessible;
import org.eclipse.swt.accessibility.AccessibleAdapter;
import org.eclipse.swt.accessibility.AccessibleEvent;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
/**
* Shows the list of tabs in the tabbed property sheet page.
*
* @author Anthony Hunter
* @author Stephane Begaudeau
*/
public class EEFTabbedPropertyList extends Composite {
/**
* This constant is used to indicate NONE (for example the index of the currently selected tab).
*/
public static final int NONE = -1;
/**
* The number of spaces used to indent a tab.
*/
public static final int INDENT = 7;
/**
* The widget factory.
*/
private EEFWidgetFactory widgetFactory;
/**
* The top navigation element.
*/
private EEFTopNavigationElement topNavigationElement;
/**
* The bottom navigation element.
*/
private EEFBottomNavigationElement bottomNavigationElement;
/**
* Boolean used to indicate if the list has the focus.
*/
private boolean focus;
/**
* The index of the selected element.
*/
private int selectedElementIndex = NONE;
/**
* The index of the widest label.
*/
private int widestLabelIndex = NONE;
/**
* The index of the top visible element.
*/
private int topVisibleIndex = NONE;
/**
* The index of the bottom visible element.
*/
private int bottomVisibleIndex = NONE;
/**
* The elements displayed.
*/
private List<EEFListElement> elements = new ArrayList<EEFListElement>();
/**
* The color holder.
*/
private EEFTabbedPropertyListColorHolder colorHolder;
/**
* The number of tabs that can fit in the composite.
*/
private int tabsThatFitInComposite;
/**
* This map specifies the number of dynamic images for a tab. It has a ITabItem as key and number of dynamic images
* for the tab as value. It is set using the setDynamicImageCount() method. It is used to calculate the width of the
* widest tab by setting aside enough space for displaying the dynamic images. Individual dynamic images are
* displayed/removed from a tab by using the showDynamicImage() and hideDynamicImage() methods on the tab's
* ListElement object.
*/
private Map<IEEFTabItem, Integer> tabToDynamicImageCountMap = new HashMap<IEEFTabItem, Integer>();
/**
* The constructor.
*
* @param parent
* The parent composite
* @param widgetFactory
* The widget factory
*/
public EEFTabbedPropertyList(Composite parent, EEFWidgetFactory widgetFactory) {
super(parent, SWT.NO_FOCUS);
this.widgetFactory = widgetFactory;
this.removeAll();
this.setLayout(new FormLayout());
this.initColors();
this.initAccessible();
this.topNavigationElement = new EEFTopNavigationElement(this);
this.bottomNavigationElement = new EEFBottomNavigationElement(this);
this.initListeners();
}
/**
* Remove all the elements from this list.
*/
public void removeAll() {
if (this.elements != null) {
for (EEFListElement element : elements) {
element.dispose();
}
}
this.elements = new ArrayList<EEFListElement>();
this.selectedElementIndex = NONE;
this.widestLabelIndex = NONE;
this.topVisibleIndex = NONE;
this.bottomVisibleIndex = NONE;
}
/**
* Initialize all the colors.
*/
private void initColors() {
this.colorHolder = new EEFTabbedPropertyListColorHolder(this.widgetFactory);
}
/**
* Calculate the number of tabs that will fit in the tab list composite.
*/
@SuppressWarnings({ "checkstyle:magicnumber" })
protected void computeTabsThatFitInComposite() {
tabsThatFitInComposite = Math.round((getSize().y - 22) / getTabHeight());
if (tabsThatFitInComposite <= 0) {
tabsThatFitInComposite = 1;
}
}
/**
* Get the height of a tab. The height of the tab is the height of the text plus buffer.
*
* @return the height of a tab.
*/
@SuppressWarnings({ "checkstyle:magicnumber" })
private int getTabHeight() {
int tabHeight = getTextDimension("").y + INDENT; //$NON-NLS-1$
if (tabsThatFitInComposite == 1) {
/*
* if only one tab will fix, reduce the size of the tab height so that the navigation elements fit.
*/
int ret = getBounds().height - 20;
int result = ret;
if (ret > tabHeight) {
result = tabHeight;
} else if (ret < 5) {
result = 5;
}
return result;
}
return tabHeight;
}
/**
* Get the dimensions of the provided string.
*
* @param text
* the string.
* @return the dimensions of the provided string.
*/
private Point getTextDimension(String text) {
GC gc = new GC(this);
gc.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT));
Point point = gc.textExtent(text);
point.x++;
gc.dispose();
return point;
}
/**
* Returns the number of elements in this list viewer.
*
* @return number of elements
*/
public int getNumberOfElements() {
return elements.size();
}
/**
* Returns the element with the given index from this list viewer. Returns <code>null</code> if the index is out of
* range.
*
* @param index
* the zero-based index
* @return the element at the given index, or <code>null</code> if the index is out of range
*/
public EEFListElement getElementAt(int index) {
if (index >= 0 && index < elements.size()) {
return elements.get(index);
}
return null;
}
/**
* Returns the zero-relative index of the item which is currently selected in the receiver, or -1 if no item is
* selected.
*
* @return the index of the selected item
*/
public int getSelectionIndex() {
return selectedElementIndex;
}
/**
* Returns zero-relative index of the widest item, or -1 if this list is empty.
*
* @return zero-relative index of the widest item, or -1 if this list is empty.
*/
public int getWidestLabelIndex() {
return widestLabelIndex;
}
/**
* Sets a map containing an IEEFTabItem as key and number of dynamic images as value. It is used to calculate the
* width of the widest tab by setting aside enough space (16 pixels per image) for displaying the dynamic images.
* Individual dynamic images are displayed/removed from a tab by using the showDynamicImage() and hideDynamicImage()
* methods on the tab's ListElement object.
*
* @param map
* The new map of tabs to dynamic image count
*/
public void setDynamicImageCount(Map<IEEFTabItem, Integer> map) {
this.tabToDynamicImageCountMap = map;
}
/**
* Sets the new list elements.
*
* @param children
* The children
*/
public void setElements(Object[] children) {
if (elements.size() != 0) {
removeAll();
}
elements = new ArrayList<EEFListElement>(children.length);
if (children.length == 0) {
widestLabelIndex = NONE;
} else {
widestLabelIndex = 0;
for (int i = 0; i < children.length; i++) {
int dynamicImageCount = 0;
if (tabToDynamicImageCountMap != null && tabToDynamicImageCountMap.containsKey(children[i])) {
dynamicImageCount = tabToDynamicImageCountMap.get(children[i]).intValue();
}
EEFListElement element = new EEFListElement(this, (IEEFTabItem) children[i], dynamicImageCount, i, this);
element.setVisible(false);
element.setLayoutData(null);
elements.add(element);
if (i != widestLabelIndex) {
int width = getTabWidth((IEEFTabItem) children[i]);
if (width > getTabWidth((IEEFTabItem) children[widestLabelIndex])) {
widestLabelIndex = i;
}
}
}
}
computeTopAndBottomTab();
}
/**
*
* Returns the width of the tab.
*
* @param tabItem
* The tab
* @return The width of the tab
*/
@SuppressWarnings({ "checkstyle:magicnumber" })
private int getTabWidth(IEEFTabItem tabItem) {
int width = getTextDimension(tabItem.getText()).x;
/*
* To anticipate for the icon placement we should always keep the space available after the label. So when the
* active tab includes an icon the width of the tab doesn't change.
*/
if (tabItem.getImage() != null) {
width = width + 16 + 4;
}
if (tabItem.isIndented()) {
width = width + INDENT;
}
if (tabToDynamicImageCountMap != null) {
int dynamicImageCount = 0;
if (tabToDynamicImageCountMap.containsKey(tabItem)) {
dynamicImageCount = tabToDynamicImageCountMap.get(tabItem).intValue();
}
if (dynamicImageCount > 0) {
/*
* Keep some space between tab's text and first dynamic image
*/
width = width + 4;
width = width + (dynamicImageCount * 16);
/*
* Keep some space between consecutive dynamic images
*/
width = width + ((dynamicImageCount - 1) * 3);
}
}
return width;
}
/**
* Selects one of the elements in the list.
*
* @param index
* the index of the element to select.
*/
public void select(int index) {
if (getSelectionIndex() == index) {
/*
* this index is already selected.
*/
return;
}
if (index >= 0 && index < elements.size()) {
int lastSelected = getSelectionIndex();
elements.get(index).setSelected(true);
selectedElementIndex = index;
if (lastSelected != NONE) {
elements.get(lastSelected).setSelected(false);
if (getSelectionIndex() != elements.size() - 1) {
/*
* redraw the next tab to fix the border by calling setSelected()
*/
elements.get(getSelectionIndex() + 1).setSelected(false);
}
}
topNavigationElement.redraw();
bottomNavigationElement.redraw();
if (selectedElementIndex < topVisibleIndex || selectedElementIndex > bottomVisibleIndex) {
computeTopAndBottomTab();
}
}
notifyListeners(SWT.Selection, new Event());
}
/**
* Deselects all the elements in the list.
*/
public void deselectAll() {
if (getSelectionIndex() != NONE) {
elements.get(getSelectionIndex()).setSelected(false);
selectedElementIndex = NONE;
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.swt.widgets.Composite#computeSize(int, int, boolean)
*/
@Override
public Point computeSize(int wHint, int hHint, boolean changed) {
Point result = super.computeSize(hHint, wHint, changed);
if (widestLabelIndex == -1) {
String propertiesNotAvailable = "Properties not available"; //$NON-NLS-1$
result.x = getTextDimension(propertiesNotAvailable).x + INDENT;
} else {
/*
* Add INDENT pixels to the left of the longest tab as a margin.
*/
int width = getTabWidth(elements.get(widestLabelIndex).getTabItem()) + INDENT;
/*
* Add 10 pixels to the right of the longest tab as a margin.
*/
result.x = width + 10;
}
return result;
}
/**
* Determine if a downward scrolling is required.
*
* @return true if downward scrolling is required.
*/
public boolean isDownScrollRequired() {
return elements.size() > tabsThatFitInComposite && bottomVisibleIndex != elements.size() - 1;
}
/**
* Determine if an upward scrolling is required.
*
* @return true if upward scrolling is required.
*/
public boolean isUpScrollRequired() {
return elements.size() > tabsThatFitInComposite && topVisibleIndex != 0;
}
/**
* Compute the top and bottom tab.
*/
private void computeTopAndBottomTab() {
computeTabsThatFitInComposite();
if (elements.size() == 0) {
/*
* no tabs to display.
*/
topVisibleIndex = 0;
bottomVisibleIndex = 0;
} else if (tabsThatFitInComposite >= elements.size()) {
/*
* all the tabs fit.
*/
topVisibleIndex = 0;
bottomVisibleIndex = elements.size() - 1;
} else if (getSelectionIndex() == NONE) {
/*
* there is no selected tab yet, assume that tab one would be selected for now.
*/
topVisibleIndex = 0;
bottomVisibleIndex = tabsThatFitInComposite - 1;
} else if (getSelectionIndex() + tabsThatFitInComposite > elements.size()) {
/*
* the selected tab is near the bottom.
*/
bottomVisibleIndex = elements.size() - 1;
topVisibleIndex = bottomVisibleIndex - tabsThatFitInComposite + 1;
} else {
/*
* the selected tab is near the top.
*/
topVisibleIndex = selectedElementIndex;
bottomVisibleIndex = selectedElementIndex + tabsThatFitInComposite - 1;
}
layoutTabs();
}
/**
* Layout the tabs.
*/
public void layoutTabs() {
// System.out.println("TabFit " + tabsThatFitInComposite + " length "
// + elements.length + " top " + topVisibleIndex + " bottom "
// + bottomVisibleIndex);
if (tabsThatFitInComposite == NONE || elements.size() == 0) {
FormData formData = new FormData();
formData.left = new FormAttachment(0, 0);
formData.right = new FormAttachment(100, 0);
formData.top = new FormAttachment(0, 0);
formData.height = getTabHeight();
topNavigationElement.setLayoutData(formData);
formData = new FormData();
formData.left = new FormAttachment(0, 0);
formData.right = new FormAttachment(100, 0);
formData.top = new FormAttachment(topNavigationElement, 0);
formData.bottom = new FormAttachment(100, 0);
bottomNavigationElement.setLayoutData(formData);
} else {
FormData formData = new FormData();
formData.left = new FormAttachment(0, 0);
formData.right = new FormAttachment(100, 0);
formData.top = new FormAttachment(0, 0);
formData.height = 10;
topNavigationElement.setLayoutData(formData);
/*
* use nextElement to attach the layout to the previous canvas widget in the list.
*/
Canvas nextElement = topNavigationElement;
for (int i = 0; i < elements.size(); i++) {
// System.out.print(i + " [" + elements[i].getText() + "]");
if (i < topVisibleIndex || i > bottomVisibleIndex) {
/*
* this tab is not visible
*/
elements.get(i).setLayoutData(null);
elements.get(i).setVisible(false);
} else {
/*
* this tab is visible.
*/
// System.out.print(" visible");
formData = new FormData();
formData.height = getTabHeight();
formData.left = new FormAttachment(0, 0);
formData.right = new FormAttachment(100, 0);
formData.top = new FormAttachment(nextElement, 0);
nextElement = elements.get(i);
elements.get(i).setLayoutData(formData);
elements.get(i).setVisible(true);
}
// if (i == selectedElementIndex) {
// System.out.print(" selected");
// }
// System.out.println("");
}
formData = new FormData();
formData.left = new FormAttachment(0, 0);
formData.right = new FormAttachment(100, 0);
formData.top = new FormAttachment(nextElement, 0);
formData.bottom = new FormAttachment(100, 0);
formData.height = 10;
bottomNavigationElement.setLayoutData(formData);
}
// System.out.println("");
// layout so that we have enough space for the new labels
Composite grandparent = getParent().getParent();
grandparent.layout(true);
layout(true);
}
public EEFTabbedPropertyListColorHolder getColorHolder() {
return this.colorHolder;
}
/**
* Return the topVisibleIndex.
*
* @return the topVisibleIndex
*/
public int getTopVisibleIndex() {
return this.topVisibleIndex;
}
/**
* Sets the topVisibleIndex.
*
* @param topVisibleIndex
* the topVisibleIndex to set
*/
public void setTopVisibleIndex(int topVisibleIndex) {
this.topVisibleIndex = topVisibleIndex;
}
/**
* Return the bottomVisibleIndex.
*
* @return the bottomVisibleIndex
*/
public int getBottomVisibleIndex() {
return this.bottomVisibleIndex;
}
/**
* Sets the bottomVisibleIndex.
*
* @param bottomVisibleIndex
* the bottomVisibleIndex to set
*/
public void setBottomVisibleIndex(int bottomVisibleIndex) {
this.bottomVisibleIndex = bottomVisibleIndex;
}
/**
* Return the topNavigationElement.
*
* @return the topNavigationElement
*/
public EEFTopNavigationElement getTopNavigationElement() {
return this.topNavigationElement;
}
/**
* Return the bottomNavigationElement.
*
* @return the bottomNavigationElement
*/
public EEFBottomNavigationElement getBottomNavigationElement() {
return this.bottomNavigationElement;
}
/**
* Return the focus.
*
* @return the focus
*/
public boolean getFocus() {
return this.focus;
}
/**
* Sets the focus.
*
* @param focus
* the focus to set
*/
public void setFocus(boolean focus) {
this.focus = focus;
}
/**
* Initialize the accessibility behavior.
*/
private void initAccessible() {
final Accessible accessible = this.getAccessible();
accessible.addAccessibleListener(new AccessibleAdapter() {
@Override
public void getName(AccessibleEvent event) {
int index = EEFTabbedPropertyList.this.getSelectionIndex();
if (index != NONE) {
event.result = EEFTabbedPropertyList.this.elements.get(index).getTabItem().getText();
}
}
@Override
public void getHelp(AccessibleEvent event) {
int index = EEFTabbedPropertyList.this.getSelectionIndex();
if (index != NONE) {
event.result = EEFTabbedPropertyList.this.elements.get(index).getTabItem().getText();
}
}
});
accessible.addAccessibleControlListener(new EEFAccessibleControlAdapter(this));
this.addListener(SWT.Selection, new Listener() {
@Override
public void handleEvent(Event event) {
if (EEFTabbedPropertyList.this.isFocusControl()) {
accessible.setFocus(ACC.CHILDID_SELF);
}
}
});
this.addListener(SWT.FocusIn, new Listener() {
@Override
public void handleEvent(Event event) {
accessible.setFocus(ACC.CHILDID_SELF);
}
});
}
/**
* Initialize the listener of the property list.
*/
private void initListeners() {
this.addFocusListener(new EEFPropertyListFocusListener(this));
this.addControlListener(new ControlAdapter() {
/**
* {@inheritDoc}
*
* @see org.eclipse.swt.events.ControlAdapter#controlResized(org.eclipse.swt.events.ControlEvent)
*/
@Override
public void controlResized(ControlEvent e) {
EEFTabbedPropertyList.this.computeTopAndBottomTab();
}
});
this.addTraverseListener(new TraverseListener() {
@Override
public void keyTraversed(TraverseEvent event) {
if (event.detail == SWT.TRAVERSE_ARROW_PREVIOUS || event.detail == SWT.TRAVERSE_ARROW_NEXT) {
int nMax = elements.size() - 1;
int nCurrent = EEFTabbedPropertyList.this.getSelectionIndex();
if (event.detail == SWT.TRAVERSE_ARROW_PREVIOUS) {
nCurrent -= 1;
nCurrent = Math.max(0, nCurrent);
} else if (event.detail == SWT.TRAVERSE_ARROW_NEXT) {
nCurrent += 1;
nCurrent = Math.min(nCurrent, nMax);
}
EEFTabbedPropertyList.this.select(nCurrent);
EEFTabbedPropertyList.this.redraw();
} else {
event.doit = true;
}
}
});
}
}