/******************************************************************************* * Copyright (c) 2008, 2018 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 * Pawel Piech (Wind River) - adapted breadcrumb for use in Debug view (Bug 252677) *******************************************************************************/ package org.eclipse.debug.internal.ui.viewers.breadcrumb; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.ListenerList; import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.IContentProvider; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ITreePathContentProvider; import org.eclipse.jface.viewers.ITreePathLabelProvider; import org.eclipse.jface.viewers.OpenEvent; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredViewer; import org.eclipse.jface.viewers.TreePath; import org.eclipse.jface.viewers.TreeSelection; import org.eclipse.jface.viewers.ViewerLabel; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.MenuDetectEvent; import org.eclipse.swt.events.MenuDetectListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Widget; /** * A breadcrumb viewer shows a the parent chain of its input element in a list. Each breadcrumb item * of that list can be expanded and a sibling of the element presented by the breadcrumb item can be * selected. *

* Content providers for breadcrumb viewers must implement the ITreePathContentProvider * interface. *

*

* Label providers for breadcrumb viewers must implement the ITreePathLabelProvider interface. *

* * @since 3.5 */ public abstract class BreadcrumbViewer extends StructuredViewer { private static final boolean IS_GTK= "gtk".equals(SWT.getPlatform()); //$NON-NLS-1$ private final int fStyle; private final Composite fContainer; private final ArrayList fBreadcrumbItems; private final ListenerList fMenuListeners; private Image fGradientBackground; private BreadcrumbItem fSelectedItem; /** * Create a new BreadcrumbViewer. *

* Style is one of: *

* * @param parent the container for the viewer * @param style the style flag used for this viewer */ public BreadcrumbViewer(Composite parent, int style) { fStyle = style; fBreadcrumbItems = new ArrayList<>(); fMenuListeners= new ListenerList<>(); fContainer= new Composite(parent, SWT.NONE); GridData layoutData= new GridData(SWT.FILL, SWT.TOP, true, false); fContainer.setLayoutData(layoutData); fContainer.addTraverseListener(e -> e.doit = true); fContainer.setBackgroundMode(SWT.INHERIT_DEFAULT); hookControl(fContainer); int columns= 1000; if ((SWT.VERTICAL & style) != 0) { columns= 2; } GridLayout gridLayout= new GridLayout(columns, false); gridLayout.marginWidth= 0; gridLayout.marginHeight= 0; gridLayout.verticalSpacing= 0; gridLayout.horizontalSpacing= 0; fContainer.setLayout(gridLayout); fContainer.addListener(SWT.Resize, event -> { updateSize(); fContainer.layout(true, true); }); } int getStyle() { return fStyle; } /** * Configure the given drop down viewer. The given input is used for the viewers input. Clients * must at least set the label and the content provider for the viewer. * @param parent the parent composite * @param site the site to create the drop down for * @param path the path to show * @return the drop down control */ protected abstract Control createDropDown(Composite parent, IBreadcrumbDropDownSite site, TreePath path); /* * @see org.eclipse.jface.viewers.Viewer#getControl() */ @Override public Control getControl() { return fContainer; } /* * @see org.eclipse.jface.viewers.StructuredViewer#reveal(java.lang.Object) */ @Override public void reveal(Object element) { //all elements are always visible } /** * Transfers the keyboard focus into the viewer. */ public void setFocus() { fContainer.setFocus(); if (fSelectedItem != null) { fSelectedItem.setFocus(true); } else { if (fBreadcrumbItems.size() == 0) { return; } BreadcrumbItem item= fBreadcrumbItems.get(fBreadcrumbItems.size() - 1); item.setFocus(true); } } /** * @return true if any of the items in the viewer is expanded */ public boolean isDropDownOpen() { for (int i= 0, size= fBreadcrumbItems.size(); i < size; i++) { BreadcrumbItem item= fBreadcrumbItems.get(i); if (item.isMenuShown()) { return true; } } return false; } /** * The shell used for the shown drop down or null * if no drop down is shown at the moment. * * @return the drop downs shell or null */ public Shell getDropDownShell() { for (int i= 0, size= fBreadcrumbItems.size(); i < size; i++) { BreadcrumbItem item= fBreadcrumbItems.get(i); if (item.isMenuShown()) { return item.getDropDownShell(); } } return null; } /** * Add the given listener to the set of listeners which will be informed * when a context menu is requested for a breadcrumb item. * * @param listener the listener to add */ public void addMenuDetectListener(MenuDetectListener listener) { fMenuListeners.add(listener); } /** * Remove the given listener from the set of menu detect listeners. * Does nothing if the listener is not element of the set. * * @param listener the listener to remove */ public void removeMenuDetectListener(MenuDetectListener listener) { fMenuListeners.remove(listener); } /* * @see org.eclipse.jface.viewers.StructuredViewer#assertContentProviderType(org.eclipse.jface.viewers.IContentProvider) */ @Override protected void assertContentProviderType(IContentProvider provider) { super.assertContentProviderType(provider); Assert.isTrue(provider instanceof ITreePathContentProvider); } /* * @see org.eclipse.jface.viewers.Viewer#inputChanged(java.lang.Object, java.lang.Object) */ @Override protected void inputChanged(final Object input, Object oldInput) { if (fContainer.isDisposed()) { return; } disableRedraw(); try { preservingSelection(() -> buildItemChain(input)); } finally { enableRedraw(); } } /* * @see org.eclipse.jface.viewers.StructuredViewer#doFindInputItem(java.lang.Object) */ @Override protected Widget doFindInputItem(Object element) { if (element == null) { return null; } if (element == getInput() || element.equals(getInput())) { return doFindItem(element); } return null; } /* * @see org.eclipse.jface.viewers.StructuredViewer#doFindItem(java.lang.Object) */ @Override protected Widget doFindItem(Object element) { if (element == null) { return null; } for (int i= 0, size= fBreadcrumbItems.size(); i < size; i++) { BreadcrumbItem item= fBreadcrumbItems.get(i); if (item.getData() == element || element.equals(item.getData())) { return item; } } return null; } /* * @see org.eclipse.jface.viewers.StructuredViewer#doUpdateItem(org.eclipse.swt.widgets.Widget, java.lang.Object, boolean) */ @Override protected void doUpdateItem(Widget widget, Object element, boolean fullMap) { myDoUpdateItem(widget, element, fullMap); } private boolean myDoUpdateItem(Widget widget, Object element, boolean fullMap) { if (widget instanceof BreadcrumbItem) { final BreadcrumbItem item= (BreadcrumbItem) widget; // remember element we are showing if (fullMap) { associate(element, item); } else { Object data= item.getData(); if (data != null) { unmapElement(data, item); } item.setData(element); mapElement(element, item); } refreshItem(item); } return false; } /** * This implementation of getSelection() returns an instance of * ITreeSelection. * @return the current selection */ @Override public ISelection getSelection() { Control control = getControl(); if (control == null || control.isDisposed()) { return TreeSelection.EMPTY; } if (fSelectedItem != null) { TreePath path = getTreePathFromItem(fSelectedItem); if (path != null) { return new TreeSelection(new TreePath[] { path }); } } return TreeSelection.EMPTY; } protected TreePath getTreePathFromItem(BreadcrumbItem item) { List elements = new ArrayList<>(fBreadcrumbItems.size()); for (int i = 0; i < fBreadcrumbItems.size(); i++) { elements.add( fBreadcrumbItems.get(i).getData() ); if (fBreadcrumbItems.get(i).equals(item)) { return new TreePath(elements.toArray()); } } return null; } /* * @see org.eclipse.jface.viewers.StructuredViewer#getSelectionFromWidget() */ @Override protected List getSelectionFromWidget() { if (fSelectedItem == null) { return Collections.EMPTY_LIST; } if (fSelectedItem.getData() == null) { return Collections.EMPTY_LIST; } ArrayList result = new ArrayList<>(); result.add(fSelectedItem.getData()); return result; } /* * @see org.eclipse.jface.viewers.StructuredViewer#internalRefresh(java.lang.Object) */ @Override protected void internalRefresh(Object element) { disableRedraw(); try { boolean layoutChanged = false; BreadcrumbItem item= (BreadcrumbItem) doFindItem(element); if (item == null || element != null && element.equals(getInput())) { for (int i= 0, size= fBreadcrumbItems.size(); i < size; i++) { BreadcrumbItem item1= fBreadcrumbItems.get(i); layoutChanged = refreshItem(item1) || layoutChanged; } } else { layoutChanged = refreshItem(item) || layoutChanged; } if (layoutChanged) { updateSize(); fContainer.layout(true, true); } } finally { enableRedraw(); } } /* * @see org.eclipse.jface.viewers.StructuredViewer#setSelectionToWidget(java.util.List, boolean) */ @Override protected void setSelectionToWidget(List l, boolean reveal) { BreadcrumbItem focusItem= null; // Unselect the currently selected items, and remember the focused item. for (int i= 0, size= fBreadcrumbItems.size(); i < size; i++) { BreadcrumbItem item= fBreadcrumbItems.get(i); if (item.hasFocus()) { focusItem= item; } item.setSelected(false); } if (l == null) { l = Collections.EMPTY_LIST; } // Set the new selection to items. fSelectedItem = null; for (Iterator iterator = l.iterator(); iterator.hasNext();) { Object element= iterator.next(); BreadcrumbItem item= (BreadcrumbItem) doFindItem(element); if (item != null) { item.setSelected(true); fSelectedItem= item; if (item == focusItem) { focusItem = null; } } } // If there is a new selection, and it does not overlap the old selection, // remove the focus marker from the old focus item. if (fSelectedItem != null && focusItem != null) { focusItem.setFocus(false); } } /** * Set a single selection to the given item. null to deselect all. * * @param item the item to select or null */ void selectItem(BreadcrumbItem item) { if (fSelectedItem != null) { fSelectedItem.setSelected(false); } fSelectedItem= item; setSelectionToWidget(getSelection(), false); setFocus(); fireSelectionChanged(new SelectionChangedEvent(this, getSelection())); } /** * Returns the item count. * * @return number of items shown in the viewer */ int getItemCount() { return fBreadcrumbItems.size(); } /** * Returns the item for the given item index. * * @param index the index of the item * @return the item ad the given index */ BreadcrumbItem getItem(int index) { return fBreadcrumbItems.get(index); } /** * Returns the index of the given item. * * @param item the item to search * @return the index of the item or -1 if not found */ int getIndexOfItem(BreadcrumbItem item) { for (int i= 0, size= fBreadcrumbItems.size(); i < size; i++) { BreadcrumbItem pItem= fBreadcrumbItems.get(i); if (pItem == item) { return i; } } return -1; } /** * Notifies all double click listeners. */ void fireDoubleClick() { fireDoubleClick(new DoubleClickEvent(this, getSelection())); } /** * Notifies all open listeners. */ void fireOpen() { fireOpen(new OpenEvent(this, getSelection())); } /** * The given element was selected from a drop down menu. * * @param selection the selected element */ void fireMenuSelection(ISelection selection) { fireOpen(new OpenEvent(this, selection)); } /** * A context menu has been requested for the selected breadcrumb item. * * @param event the event issued the menu detection */ void fireMenuDetect(MenuDetectEvent event) { for (MenuDetectListener menuDetectListener : fMenuListeners) { menuDetectListener.menuDetected(event); } } /** * Set selection to the next or previous element if possible. * * @param next true if the next element should be selected, otherwise the previous * one will be selected */ void doTraverse(boolean next) { if (fSelectedItem == null) { return; } int index= fBreadcrumbItems.indexOf(fSelectedItem); if (next) { if (index == fBreadcrumbItems.size() - 1) { BreadcrumbItem current= fBreadcrumbItems.get(index); current.openDropDownMenu(); current.getDropDownShell().setFocus(); } else { BreadcrumbItem nextItem= fBreadcrumbItems.get(index + 1); selectItem(nextItem); } } else { if (index == 0) { BreadcrumbItem root= fBreadcrumbItems.get(index); root.openDropDownMenu(); root.getDropDownShell().setFocus(); } else { selectItem(fBreadcrumbItems.get(index - 1)); } } } /** * Generates the parent chain of the given element. * @param input element to build the parent chain for */ private void buildItemChain(Object input) { if (fBreadcrumbItems.size() > 0) { BreadcrumbItem last= fBreadcrumbItems.get(fBreadcrumbItems.size() - 1); last.setIsLastItem(false); } int index = 0; boolean updateLayout = false; if (input != null) { ITreePathContentProvider contentProvider= (ITreePathContentProvider) getContentProvider(); TreePath path = new TreePath(new Object[0]); // Top level elements need to be retrieved using getElements(), rest // using getChildren(). Object[] children = contentProvider.getElements(input); Object element = children != null && children.length != 0 ? children[0] : null; while (element != null) { path = path.createChildPath(element); // All but last item are hidden if the viewer is in a vertical toolbar. children = contentProvider.getChildren(path); if ((getStyle() & SWT.VERTICAL) == 0 || children == null || children.length == 0) { updateLayout = updateOrCreateItem(index++, path, element) || updateLayout; } if (children != null && children.length != 0) { element = children[0]; } else { break; } } } BreadcrumbItem last = null; if (index <= fBreadcrumbItems.size()) { last = (fBreadcrumbItems.get(index - 1)); last.setIsLastItem(true); } while (index < fBreadcrumbItems.size()) { updateLayout = true; BreadcrumbItem item= fBreadcrumbItems.remove(fBreadcrumbItems.size() - 1); if (item.hasFocus() && last != null) { last.setFocus(true); } if (item == fSelectedItem) { selectItem(null); } if (item.getData() != null) { unmapElement(item.getData()); } item.dispose(); } if (updateLayout) { updateSize(); fContainer.layout(true, true); } } /** * @param item Item to refresh. * @return returns whether the item's size and layout needs to be updated. */ private boolean refreshItem(BreadcrumbItem item) { boolean layoutChanged = false; TreePath path = getTreePathFromItem(item); ViewerLabel label = new ViewerLabel(item.getText(), item.getImage()); ((ITreePathLabelProvider)getLabelProvider()).updateLabel(label, path); if (label.hasNewText()) { item.setText(label.getText()); layoutChanged = true; } if (label.hasNewImage()) { item.setImage(label.getImage()); layoutChanged = true; } if (label.hasNewTooltipText()) { item.setToolTip(label.getTooltipText()); } return layoutChanged; } /** * Creates or updates a breadcrumb item. * @param index the index * @param path the path * @param element the element * * @return whether breadcrumb layout needs to be updated due to this change */ private boolean updateOrCreateItem(int index, TreePath path, Object element) { BreadcrumbItem item; if (fBreadcrumbItems.size() > index) { item = fBreadcrumbItems.get(index); if (item.getData() != null) { unmapElement(item.getData()); } } else { item = new BreadcrumbItem(this, fContainer); fBreadcrumbItems.add(item); } boolean updateLayout = false; if (equals(element, item.getData())) { item.setPath(path); updateLayout = myDoUpdateItem(item, element, false); } else { item.setData(element); item.setPath(path); mapElement(element, item); updateLayout = refreshItem(item); } return updateLayout; } /** * Update the size of the items such that all items are visible, if possible. * * @return true if any item has changed, false otherwise */ private boolean updateSize() { int width= fContainer.getClientArea().width; int currentWidth= getCurrentWidth(); boolean requiresLayout= false; if (currentWidth > width) { int index= 0; while (currentWidth > width && index < fBreadcrumbItems.size() - 1) { BreadcrumbItem viewer= fBreadcrumbItems.get(index); if (viewer.isShowText()) { viewer.setShowText(false); currentWidth= getCurrentWidth(); requiresLayout= true; } index++; } } else if (currentWidth < width) { int index= fBreadcrumbItems.size() - 1; while (currentWidth < width && index >= 0) { BreadcrumbItem viewer= fBreadcrumbItems.get(index); if (!viewer.isShowText()) { viewer.setShowText(true); currentWidth= getCurrentWidth(); if (currentWidth > width) { viewer.setShowText(false); index= 0; } else { requiresLayout= true; } } index--; } } return requiresLayout; } /** * Returns the current width of all items in the list. * * @return the width of all items in the list */ private int getCurrentWidth() { int result= 0; for (int i= 0, size= fBreadcrumbItems.size(); i < size; i++) { BreadcrumbItem viewer= fBreadcrumbItems.get(i); result+= viewer.getWidth(); } return result; } /** * Enables redrawing of the breadcrumb. */ private void enableRedraw() { if (IS_GTK) { return; } fContainer.setRedraw(true); } /** * Disables redrawing of the breadcrumb. * *

* A call to this method must be followed by a call to {@link #enableRedraw()} *

*/ private void disableRedraw() { if (IS_GTK) { return; } fContainer.setRedraw(false); } /** * Blends c1 and c2 based in the provided ratio. * * @param c1 * first color * @param c2 * second color * @param ratio * percentage of the first color in the blend (0-100) * @return the RGB value of the blended color * @since 3.1 */ public static RGB blend(RGB c1, RGB c2, int ratio) { int r = blend(c1.red, c2.red, ratio); int g = blend(c1.green, c2.green, ratio); int b = blend(c1.blue, c2.blue, ratio); return new RGB(r, g, b); } /** * Blends two primary color components based on the provided ratio. * * @param v1 * first component * @param v2 * second component * @param ratio * percentage of the first component in the blend * @return the blended color */ private static int blend(int v1, int v2, int ratio) { int b = (ratio * v1 + (100 - ratio) * v2) / 100; return Math.min(255, b); } /* * @see * org.eclipse.jface.viewers.StructuredViewer#handleDispose(org.eclipse.swt.events.DisposeEvent) * * @since 3.7 */ @Override protected void handleDispose(DisposeEvent event) { if (fGradientBackground != null) { fGradientBackground.dispose(); fGradientBackground= null; } if (fBreadcrumbItems != null) { for (BreadcrumbItem item : fBreadcrumbItems) { item.dispose(); } } super.handleDispose(event); } }