diff --git a/bundles/org.eclipse.jface/src/org/eclipse/jface/messages.properties b/bundles/org.eclipse.jface/src/org/eclipse/jface/messages.properties
index 2dbb4c71ffb..4c8b01ed4a2 100644
--- a/bundles/org.eclipse.jface/src/org/eclipse/jface/messages.properties
+++ b/bundles/org.eclipse.jface/src/org/eclipse/jface/messages.properties
@@ -198,6 +198,11 @@ F20 = F20
SafeRunnable.errorMessage = An error has occurred. See error log for more details.
ColorSelector.Name=Color Selector
+#############################################################
+# org.eclipse.jface.viewers
+#############################################################
+BreadcrumbItemDropDown_showDropDownMenu_action_toolTip=Show Children
+
#############################################################
# org.eclipse.jface.viewers.deferred
#############################################################
diff --git a/bundles/org.eclipse.jface/src/org/eclipse/jface/viewers/BreadcrumbItem.java b/bundles/org.eclipse.jface/src/org/eclipse/jface/viewers/BreadcrumbItem.java
new file mode 100644
index 00000000000..320274b9518
--- /dev/null
+++ b/bundles/org.eclipse.jface/src/org/eclipse/jface/viewers/BreadcrumbItem.java
@@ -0,0 +1,315 @@
+/*******************************************************************************
+ * Copyright (c) 2008, 2011 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ * Fabio Zadrozny - Bug 465666
+ *******************************************************************************/
+package org.eclipse.jface.viewers;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Item;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * An item in a breadcrumb viewer.
+ *
+ * The item shows a label and an image. It also has the ability to expand, that
+ * is to open a drop down menu.
+ *
+ *
+ * The drop down allows to select any child of the items input element. The item
+ * shows the label and icon of its data element, if any.
+ *
+ *
+ * @since 3.41
+ */
+class BreadcrumbItem extends Item {
+
+ private final BreadcrumbViewer fParent;
+ private Composite fContainer;
+
+ private BreadcrumbItemDetails fDetailsBlock;
+ private BreadcrumbItemDropDown fExpandBlock;
+ private boolean fIsLast;
+
+ /**
+ * A new breadcrumb item which is shown inside the given viewer.
+ *
+ * @param viewer the items viewer
+ * @param parent the container containing the item
+ */
+ public BreadcrumbItem(BreadcrumbViewer viewer, Composite parent) {
+ super(parent, SWT.NONE);
+
+ fParent = viewer;
+
+ fContainer = new Composite(parent, SWT.NONE);
+ fContainer.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false));
+ GridLayout layout = new GridLayout(2, false);
+ layout.marginBottom = 1;
+ layout.marginHeight = 0;
+ layout.marginWidth = 0;
+ layout.horizontalSpacing = 0;
+ fContainer.setLayout(layout);
+
+ fDetailsBlock = new BreadcrumbItemDetails(this, fContainer);
+
+ fExpandBlock = new BreadcrumbItemDropDown(this, fContainer);
+
+ fContainer.setData("org.eclipse.e4.ui.css.id", "BreadcrumbItemComposite"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ /**
+ * Returns this items viewer.
+ *
+ * @return the viewer showing this item
+ */
+ public BreadcrumbViewer getViewer() {
+ return fParent;
+ }
+
+ /*
+ * @see org.eclipse.swt.widgets.Widget#dispose()
+ */
+ @Override
+ public void dispose() {
+ Shell shell = fExpandBlock.getDropDownShell();
+ if (shell != null && !shell.isDisposed()) {
+ shell.dispose();
+ }
+ fContainer.dispose();
+ super.dispose();
+ }
+
+ /**
+ * Should this item show a text label.
+ *
+ * @param enabled true if it should
+ */
+ void setShowText(boolean enabled) {
+ fDetailsBlock.setTextVisible(enabled);
+ }
+
+ /**
+ * Does this item show a text label?
+ *
+ * @return true if it does.
+ */
+ boolean isShowText() {
+ return fDetailsBlock.isTextVisible();
+ }
+
+ /**
+ * Returns the width of this item.
+ *
+ * @return the width of this item
+ */
+ int getWidth() {
+ return fDetailsBlock.getWidth() + fExpandBlock.getWidth() + 2;
+ }
+
+ /**
+ * Sets whether this item has to be marked as selected or not.
+ *
+ * @param selected true if marked as selected
+ */
+ void setSelected(boolean selected) {
+ fDetailsBlock.setSelected(selected);
+ }
+
+ /**
+ * Sets whether this item has the keyboard focus.
+ *
+ * @param state true if it has focus, false otherwise
+ */
+ void setFocus(boolean state) {
+ fDetailsBlock.setFocus(state);
+ }
+
+ /**
+ * Returns whether this item has the keyboard focus.
+ *
+ * @return true if this item has the keyboard focus
+ */
+ boolean hasFocus() {
+ return fDetailsBlock.hasFocus();
+ }
+
+ /**
+ * Redraws this item using the viewer's current providers.
+ */
+ void refresh() {
+ IBaseLabelProvider labelProvider = fParent.getLabelProvider();
+
+ if (labelProvider instanceof CellLabelProvider cellLabelProvider) {
+ BreadcrumbViewerRow row = new BreadcrumbViewerRow(fParent, this);
+ ViewerCell cell = row.getCell(0);
+ cellLabelProvider.update(cell);
+ } else {
+ ILabelProvider simpleLabelProvider = (ILabelProvider) labelProvider;
+ String text = simpleLabelProvider.getText(getData());
+ Image image = simpleLabelProvider.getImage(getData());
+
+ setText(text);
+ setImage(image);
+ }
+
+ refreshToolTip();
+ refreshArrow();
+ }
+
+ void refreshToolTip() {
+ ILabelProvider toolTipLabelProvider = fParent.getToolTipLabelProvider();
+ if (toolTipLabelProvider != null) {
+ fDetailsBlock.setToolTip(toolTipLabelProvider.getText(getData()));
+ } else {
+ fDetailsBlock.setToolTip(getText());
+ }
+ }
+
+ /**
+ * Refresh the arrows visibility.
+ */
+ void refreshArrow() {
+ ITreeContentProvider contentProvider = (ITreeContentProvider) fParent.getContentProvider();
+ fExpandBlock.setEnabled(contentProvider.hasChildren(getData()));
+ }
+
+ /**
+ * Set whether this is the last item in the breadcrumb item chain or not.
+ *
+ * @param isLast true if this is the last item, false
+ * otherwise
+ */
+ void setIsLastItem(boolean isLast) {
+ fIsLast = isLast;
+
+ GridData data = (GridData) fContainer.getLayoutData();
+ data.grabExcessHorizontalSpace = isLast;
+ }
+
+ /**
+ * Sets whether or not the this item should show the details (name and label).
+ *
+ * @param visible true if the item shows details
+ */
+ void setDetailsVisible(boolean visible) {
+ fDetailsBlock.setVisible(visible);
+ }
+
+ /**
+ * Expand this item, shows the drop down menu.
+ */
+ void openDropDownMenu() {
+ fExpandBlock.showMenu();
+ }
+
+ /**
+ * @return true if this item is expanded
+ */
+ boolean isMenuShown() {
+ return fExpandBlock.isMenuShown();
+ }
+
+ /**
+ * Returns the drop down shell.
+ *
+ * @return the shell of the drop down if shown, null otherwise
+ */
+ Shell getDropDownShell() {
+ return fExpandBlock.getDropDownShell();
+ }
+
+ /**
+ * Returns the drop down selection provider of this item.
+ *
+ * @return the selection provider of the drop down or null
+ */
+ ISelectionProvider getDropDownSelectionProvider() {
+ return fExpandBlock.getDropDownSelectionProvider();
+ }
+
+ /**
+ * Returns the bounds of this item.
+ *
+ * @return the bounds of this item
+ */
+ public Rectangle getBounds() {
+ return fContainer.getBounds();
+ }
+
+ /**
+ * Set the tool tip of the item to the given text.
+ *
+ * @param text the tool tip for the item
+ */
+ public void setToolTip(String text) {
+ fDetailsBlock.setToolTip(text);
+ }
+
+ Color getForeground() {
+ return fDetailsBlock.getForeground();
+ }
+
+ void setForeground(Color color) {
+ fDetailsBlock.setForeground(color);
+ }
+
+ Color getBackground() {
+ return fDetailsBlock.getBackground();
+ }
+
+ void setBackground(Color color) {
+ fDetailsBlock.setBackground(color);
+ }
+
+ Font getFont() {
+ return fDetailsBlock.getFont();
+ }
+
+ void setFont(Font font) {
+ fDetailsBlock.setFont(font);
+ }
+
+ /*
+ * @see org.eclipse.swt.widgets.Item#setText(java.lang.String)
+ */
+ @Override
+ public void setText(String string) {
+ if (string == null) {
+ string = ""; //$NON-NLS-1$
+ }
+
+ super.setText(string);
+ fDetailsBlock.setText(string);
+
+ // more or less space might be required for the label
+ if (fIsLast)
+ fContainer.layout(true, true);
+ }
+
+ /*
+ * @see org.eclipse.swt.widgets.Item#setImage(org.eclipse.swt.graphics.Image)
+ */
+ @Override
+ public void setImage(Image image) {
+ super.setImage(image);
+ fDetailsBlock.setImage(image);
+ }
+
+}
diff --git a/bundles/org.eclipse.jface/src/org/eclipse/jface/viewers/BreadcrumbItemDetails.java b/bundles/org.eclipse.jface/src/org/eclipse/jface/viewers/BreadcrumbItemDetails.java
new file mode 100644
index 00000000000..6ab8496a992
--- /dev/null
+++ b/bundles/org.eclipse.jface/src/org/eclipse/jface/viewers/BreadcrumbItemDetails.java
@@ -0,0 +1,485 @@
+/*******************************************************************************
+ * Copyright (c) 2008, 2011 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ * Fabio Zadrozny - Bug 465666
+ *******************************************************************************/
+package org.eclipse.jface.viewers;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.accessibility.AccessibleAdapter;
+import org.eclipse.swt.accessibility.AccessibleEvent;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+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.Label;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * The label and icon part of the breadcrumb item.
+ *
+ * @since 3.41
+ */
+class BreadcrumbItemDetails {
+
+ private final Label fElementImage;
+ private final Label fElementText;
+ private final Composite fDetailComposite;
+ private final BreadcrumbItem fParent;
+ private final Composite fTextComposite;
+ private final Composite fImageComposite;
+
+ private boolean fTextVisible;
+ private boolean fSelected;
+ private boolean fHasFocus;
+
+ private Color fForeground;
+ private Color fDefaultForeground;
+ private Font fFont;
+ private Font fDefaultFont;
+ private Color fBackground;
+ private Color fDefaultDetailBackground;
+ private Color fDefaultTextBackground;
+ private Color fDefaultImageBackground;
+ private Color fDefaultElementTextBackground;
+ private Color fDefaultElementImageBackground;
+
+ public BreadcrumbItemDetails(BreadcrumbItem parent, Composite parentContainer) {
+ fParent = parent;
+ fTextVisible = true;
+
+ fDetailComposite = new Composite(parentContainer, SWT.NONE);
+ fDetailComposite.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false));
+ GridLayout layout = new GridLayout(2, false);
+ layout.marginHeight = 0;
+ layout.marginWidth = 0;
+ layout.horizontalSpacing = 0;
+ fDetailComposite.setLayout(layout);
+ addElementListener(fDetailComposite);
+
+ fImageComposite = new Composite(fDetailComposite, SWT.NONE);
+ fImageComposite.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false));
+ layout = new GridLayout(1, false);
+ layout.marginHeight = 1;
+ layout.marginWidth = 2;
+ fImageComposite.setLayout(layout);
+ fImageComposite.addPaintListener(e -> {
+ if (fHasFocus && !isTextVisible()) {
+ e.gc.drawFocus(e.x, e.y, e.width, e.height);
+ }
+ });
+ installFocusComposite(fImageComposite);
+ addElementListener(fImageComposite);
+
+ fElementImage = new Label(fImageComposite, SWT.NONE);
+ GridData layoutData = new GridData(SWT.BEGINNING, SWT.CENTER, false, false);
+ fElementImage.setLayoutData(layoutData);
+ addElementListener(fElementImage);
+
+ fTextComposite = new Composite(fDetailComposite, SWT.NONE);
+ fTextComposite.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false));
+ layout = new GridLayout(1, false);
+ layout.marginHeight = 2;
+ layout.marginWidth = 2;
+ fTextComposite.setLayout(layout);
+ addElementListener(fTextComposite);
+ fTextComposite.addPaintListener(e -> {
+ if (fHasFocus && isTextVisible()) {
+ e.gc.drawFocus(e.x, e.y, e.width, e.height);
+ }
+ });
+ installFocusComposite(fTextComposite);
+
+ fElementText = new Label(fTextComposite, SWT.NONE);
+ layoutData = new GridData(SWT.BEGINNING, SWT.CENTER, false, false);
+ fElementText.setLayoutData(layoutData);
+ addElementListener(fElementText);
+
+ fTextComposite.getAccessible().addAccessibleListener(new AccessibleAdapter() {
+ @Override
+ public void getName(AccessibleEvent e) {
+ e.result = fElementText.getText();
+ }
+ });
+ fImageComposite.getAccessible().addAccessibleListener(new AccessibleAdapter() {
+ @Override
+ public void getName(AccessibleEvent e) {
+ e.result = fElementText.getText();
+ }
+ });
+
+ fDetailComposite.setTabList(new Control[] { fTextComposite });
+
+ fDetailComposite.setData("org.eclipse.e4.ui.css.id", "BreadcrumbItemDetailComposite"); //$NON-NLS-1$ //$NON-NLS-2$
+ fTextComposite.setData("org.eclipse.e4.ui.css.id", "BreadcrumbItemDetailTextComposite"); //$NON-NLS-1$ //$NON-NLS-2$
+ fImageComposite.setData("org.eclipse.e4.ui.css.id", "BreadcrumbItemDetailImageComposite"); //$NON-NLS-1$ //$NON-NLS-2$
+ fElementImage.setData("org.eclipse.e4.ui.css.id", "BreadcrumbItemDetailImageLabel"); //$NON-NLS-1$ //$NON-NLS-2$
+ fElementText.setData("org.eclipse.e4.ui.css.id", "BreadcrumbItemDetailTextLabel"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ /**
+ * Returns whether this element has the keyboard focus.
+ *
+ * @return true if this element has the keyboard focus.
+ */
+ public boolean hasFocus() {
+ return fHasFocus;
+ }
+
+ /**
+ * Sets the tool tip to the given text.
+ *
+ * @param text the tool tip
+ */
+ public void setToolTip(String text) {
+ if (isTextVisible()) {
+ fElementText.getParent().setToolTipText(text);
+ fElementText.setToolTipText(text);
+
+ fElementImage.setToolTipText(text);
+ } else {
+ fElementText.getParent().setToolTipText(null);
+ fElementText.setToolTipText(null);
+
+ fElementImage.setToolTipText(text);
+ }
+ }
+
+ /**
+ * Sets the image to the given image.
+ *
+ * @param image the image to use
+ */
+ public void setImage(Image image) {
+ if (image != fElementImage.getImage()) {
+ fElementImage.setImage(image);
+ }
+ }
+
+ /**
+ * Sets the text to the given text.
+ *
+ * @param text the text to use
+ */
+ public void setText(String text) {
+ if (text == null) {
+ text = ""; //$NON-NLS-1$
+ }
+ if (!text.equals(fElementText.getText())) {
+ fElementText.setText(text);
+ }
+ }
+
+ public Color getForeground() {
+ return fForeground;
+ }
+
+ public void setForeground(Color color) {
+ if (fForeground == null && color == null) {
+ return;
+ }
+
+ if (fForeground == null) {
+ fDefaultForeground = fElementText.getForeground();
+ }
+
+ fForeground = color;
+ fElementText.setForeground(color != null ? color : fDefaultForeground);
+ }
+
+ public Color getBackground() {
+ return fBackground;
+ }
+
+ public void setBackground(Color color) {
+ if (fBackground == null && color == null) {
+ return;
+ }
+
+ if (fBackground == null) {
+ // Preserve the themed defaults so they can be restored when the provider
+ // returns null.
+ fDefaultDetailBackground = fDetailComposite.getBackground();
+ fDefaultTextBackground = fTextComposite.getBackground();
+ fDefaultImageBackground = fImageComposite.getBackground();
+ fDefaultElementTextBackground = fElementText.getBackground();
+ fDefaultElementImageBackground = fElementImage.getBackground();
+ }
+
+ fBackground = color;
+
+ fDetailComposite.setBackground(color != null ? color : fDefaultDetailBackground);
+ fTextComposite.setBackground(color != null ? color : fDefaultTextBackground);
+ fImageComposite.setBackground(color != null ? color : fDefaultImageBackground);
+ fElementText.setBackground(color != null ? color : fDefaultElementTextBackground);
+ fElementImage.setBackground(color != null ? color : fDefaultElementImageBackground);
+ }
+
+ public Font getFont() {
+ return fFont;
+ }
+
+ public void setFont(Font font) {
+ if (fFont == null && font == null) {
+ return;
+ }
+
+ if (fFont == null) {
+ fDefaultFont = fElementText.getFont();
+ }
+
+ fFont = font;
+ fElementText.setFont(font != null ? font : fDefaultFont);
+ }
+
+ /**
+ * Returns the width of this element.
+ *
+ * @return current width of this element
+ */
+ public int getWidth() {
+ int result = 2;
+
+ if (fElementImage.getImage() != null)
+ result += fElementImage.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
+
+ if (fTextVisible && fElementText.getText().length() > 0)
+ result += fElementText.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
+
+ return result;
+ }
+
+ public void setTextVisible(boolean enabled) {
+ if (fTextVisible == enabled)
+ return;
+
+ fTextVisible = enabled;
+
+ GridData data = (GridData) fTextComposite.getLayoutData();
+ data.exclude = !enabled;
+ fTextComposite.setVisible(enabled);
+
+ if (fTextVisible) {
+ fDetailComposite.setTabList(new Control[] { fTextComposite });
+ } else {
+ fDetailComposite.setTabList(new Control[] { fImageComposite });
+ }
+
+ if (fHasFocus) {
+ if (isTextVisible()) {
+ fTextComposite.setFocus();
+ } else {
+ fImageComposite.setFocus();
+ }
+ }
+ }
+
+ /**
+ * Tells whether this item shows a text or only an image.
+ *
+ * @return true if it shows a text and an image, false if it only
+ * shows the image
+ */
+ public boolean isTextVisible() {
+ return fTextVisible;
+ }
+
+ /**
+ * Sets whether details should be shown.
+ *
+ * @param visible true if details should be shown
+ */
+ public void setVisible(boolean visible) {
+ fDetailComposite.setVisible(visible);
+
+ GridData data = (GridData) fDetailComposite.getLayoutData();
+ data.exclude = !visible;
+ }
+
+ public void setSelected(boolean selected) {
+ if (selected == fSelected)
+ return;
+
+ fSelected = selected;
+ if (!fSelected)
+ fHasFocus = false;
+
+ }
+
+ public void setFocus(boolean enabled) {
+ if (enabled == fHasFocus)
+ return;
+
+ fHasFocus = enabled;
+ if (fHasFocus) {
+ if (isTextVisible()) {
+ fTextComposite.setFocus();
+ } else {
+ fImageComposite.setFocus();
+ }
+ }
+ }
+
+ /**
+ * Install focus and key listeners to the given composite.
+ *
+ * @param composite the composite which may get focus
+ */
+ private void installFocusComposite(Composite composite) {
+ composite.addTraverseListener(e -> {
+ if (e.detail == SWT.TRAVERSE_TAB_NEXT || e.detail == SWT.TRAVERSE_TAB_PREVIOUS) {
+ int index = fParent.getViewer().getIndexOfItem(fParent);
+ if (e.detail == SWT.TRAVERSE_TAB_NEXT) {
+ index++;
+ } else {
+ index--;
+ }
+
+ if (index > 0 && index < fParent.getViewer().getItemCount()) {
+ fParent.getViewer().selectItem(fParent.getViewer().getItem(index));
+ }
+
+ e.doit = true;
+ }
+ });
+ composite.addKeyListener(new KeyListener() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ BreadcrumbViewer viewer = fParent.getViewer();
+
+ switch (e.keyCode) {
+ case SWT.ARROW_LEFT:
+ if (fSelected) {
+ viewer.doTraverse(false);
+ e.doit = false;
+ } else {
+ viewer.selectItem(fParent);
+ }
+ break;
+ case SWT.ARROW_RIGHT:
+ if (fSelected) {
+ viewer.doTraverse(true);
+ e.doit = false;
+ } else {
+ viewer.selectItem(fParent);
+ }
+ break;
+ case SWT.ARROW_DOWN:
+ case SWT.KEYPAD_ADD:
+ if (!fSelected) {
+ viewer.selectItem(fParent);
+ }
+ openDropDown();
+ e.doit = false;
+ break;
+ case SWT.CR:
+ if (!fSelected) {
+ viewer.selectItem(fParent);
+ }
+ viewer.fireOpen();
+ break;
+ default:
+ if (e.character == ' ') {
+ if (!fSelected) {
+ viewer.selectItem(fParent);
+ }
+ openDropDown();
+ e.doit = false;
+ }
+ break;
+ }
+ }
+
+ private void openDropDown() {
+ BreadcrumbViewer viewer = fParent.getViewer();
+
+ int index = viewer.getIndexOfItem(fParent);
+ BreadcrumbItem parent = fParent.getViewer().getItem(index - 1);
+
+ Shell shell = parent.getDropDownShell();
+ if (shell == null) {
+ parent.openDropDownMenu();
+ shell = parent.getDropDownShell();
+ }
+ shell.setFocus();
+ }
+
+ @Override
+ public void keyReleased(KeyEvent e) {
+ }
+ });
+
+ composite.addFocusListener(new FocusListener() {
+ @Override
+ public void focusGained(FocusEvent e) {
+ if (!fHasFocus) {
+ fHasFocus = true;
+ }
+ }
+
+ @Override
+ public void focusLost(FocusEvent e) {
+ if (fHasFocus) {
+ fHasFocus = false;
+ }
+ }
+ });
+ }
+
+ /**
+ * Add mouse listeners to the given control.
+ *
+ * @param control the control to which may be clicked
+ */
+ private void addElementListener(Control control) {
+ control.addMouseListener(new MouseListener() {
+ @Override
+ public void mouseDoubleClick(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseDown(MouseEvent e) {
+ BreadcrumbViewer viewer = fParent.getViewer();
+
+ int parentIndex = viewer.getIndexOfItem(fParent) - 1;
+ Shell shell = null;
+ if (parentIndex >= 0) {// sanity check, must always hold
+ BreadcrumbItem dropDownItem = viewer.getItem(parentIndex);
+ shell = dropDownItem.getDropDownShell();
+ }
+
+ viewer.selectItem(fParent);
+ if (shell == null && e.button == 1 && e.stateMask == 0) {
+ fParent.getViewer().fireDoubleClick();
+ }
+ }
+
+ @Override
+ public void mouseUp(MouseEvent e) {
+ }
+ });
+ control.addMenuDetectListener(e -> {
+ BreadcrumbViewer viewer = fParent.getViewer();
+ viewer.selectItem(fParent);
+ fParent.getViewer().fireMenuDetect(e);
+ });
+ }
+}
diff --git a/bundles/org.eclipse.jface/src/org/eclipse/jface/viewers/BreadcrumbItemDropDown.java b/bundles/org.eclipse.jface/src/org/eclipse/jface/viewers/BreadcrumbItemDropDown.java
new file mode 100644
index 00000000000..a81edbb237b
--- /dev/null
+++ b/bundles/org.eclipse.jface/src/org/eclipse/jface/viewers/BreadcrumbItemDropDown.java
@@ -0,0 +1,739 @@
+/*******************************************************************************
+ * Copyright (c) 2008, 2020 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ * Fabio Zadrozny - Bug 465666
+ *******************************************************************************/
+package org.eclipse.jface.viewers;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.dialogs.IDialogSettings;
+import org.eclipse.jface.resource.CompositeImageDescriptor;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.util.OpenStrategy;
+import org.eclipse.jface.util.Util;
+import org.eclipse.swt.SWT;
+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.ControlListener;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.events.ShellListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.ImageDataProvider;
+import org.eclipse.swt.graphics.ImageGcDrawer;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+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.Display;
+import org.eclipse.swt.widgets.Item;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+import org.eclipse.swt.widgets.Widget;
+
+/**
+ * The part of the breadcrumb item with the drop down menu.
+ *
+ * @since 3.41
+ */
+class BreadcrumbItemDropDown {
+ private static final boolean IS_MAC_WORKAROUND = "carbon".equals(SWT.getPlatform()); //$NON-NLS-1$
+
+ /**
+ * An arrow image descriptor. The images color is related to the list fore- and
+ * background color. This makes the arrow visible even in high contrast mode. If
+ * ltr is true the arrow points to the right, otherwise it points
+ * to the left.
+ */
+ private final class AccessibleArrowImage extends CompositeImageDescriptor {
+
+ private final static int ARROW_SIZE = 5;
+
+ private final boolean fLTR;
+
+ public AccessibleArrowImage(boolean ltr) {
+ fLTR = ltr;
+ }
+
+ @Override
+ protected void drawCompositeImage(int width, int height) {
+ Display display = fParentComposite.getDisplay();
+
+ ImageDataProvider imageProvider = zoom -> {
+ ImageGcDrawer drawer = new ImageGcDrawer() {
+ @Override
+ public void drawOn(GC gc, int iWidth, int iHeight) {
+ gc.setAntialias(SWT.ON);
+ Color triangleColor = createColor(SWT.COLOR_LIST_FOREGROUND, SWT.COLOR_LIST_BACKGROUND, 20,
+ display);
+ gc.setBackground(triangleColor);
+ gc.fillPolygon(new int[] { 0, 0, ARROW_SIZE, ARROW_SIZE, 0, ARROW_SIZE * 2 });
+ triangleColor.dispose();
+ }
+
+ @Override
+ public int getGcStyle() {
+ return fLTR ? SWT.LEFT_TO_RIGHT : SWT.RIGHT_TO_LEFT;
+ }
+ };
+ Image image = new Image(display, drawer, ARROW_SIZE, ARROW_SIZE * 2);
+
+ ImageData imageData = image.getImageData(zoom);
+ image.dispose();
+ int zoomedArrowSize = ARROW_SIZE * zoom / 100;
+ for (int y1 = 0; y1 < zoomedArrowSize; y1++) {
+ // set opaque pixels for top half of the breadcrumb arrow
+ for (int x1 = 0; x1 <= y1; x1++) {
+ imageData.setAlpha(fLTR ? x1 : zoomedArrowSize - x1 - 1, y1, 255);
+ }
+ // set transparent pixels for top half of the breadcrumbe arrow
+ for (int x1 = y1 + 1; x1 < zoomedArrowSize; x1++) {
+ imageData.setAlpha(fLTR ? x1 : zoomedArrowSize - x1 - 1, y1, 0);
+ }
+ }
+ for (int y2 = 0; y2 < zoomedArrowSize; y2++) {
+ // set opaque pixels for bottom half of the breadcrumb arrow
+ for (int x2 = 0; x2 <= y2; x2++) {
+ imageData.setAlpha(fLTR ? x2 : zoomedArrowSize - x2 - 1, zoomedArrowSize * 2 - y2 - 1, 255);
+ }
+ // set transparent pixels for bottom half of the breadcrumbe arrow
+ for (int x2 = y2 + 1; x2 < zoomedArrowSize; x2++) {
+ imageData.setAlpha(fLTR ? x2 : zoomedArrowSize - x2 - 1, zoomedArrowSize * 2 - y2 - 1, 0);
+ }
+ }
+ return imageData;
+ };
+ drawImage(imageProvider, (width / 2) - (ARROW_SIZE / 2), (height / 2) - ARROW_SIZE);
+ }
+
+ @Override
+ protected Point getSize() {
+ return new Point(10, 16);
+ }
+
+ private Color createColor(int color1, int color2, int ratio, Display display) {
+ RGB rgb1 = display.getSystemColor(color1).getRGB();
+ RGB rgb2 = display.getSystemColor(color2).getRGB();
+
+ RGB blend = blend(rgb2, rgb1, ratio);
+
+ return new Color(blend);
+ }
+
+ private static RGB blend(RGB rgb1, RGB rgb2, int ratio) {
+ int r = (rgb1.red * ratio + rgb2.red * (100 - ratio)) / 100;
+ int g = (rgb1.green * ratio + rgb2.green * (100 - ratio)) / 100;
+ int b = (rgb1.blue * ratio + rgb2.blue * (100 - ratio)) / 100;
+
+ return new RGB(r, g, b);
+ }
+ }
+
+ private static final int DROP_DOWN_MIN_WIDTH = 250;
+ private static final int DROP_DOWN_MAX_WIDTH = 500;
+
+ private static final int DROP_DOWN_DEFAULT_MIN_HEIGHT = 200;
+ private static final int DROP_DOWN_DEFAULT_MAX_HEIGHT = 300;
+
+ private static final String DIALOG_SETTINGS = "BreadcrumbItemDropDown"; //$NON-NLS-1$
+ private static final String DIALOG_HEIGHT = "height"; //$NON-NLS-1$
+
+ private final BreadcrumbItem fParent;
+ private final Composite fParentComposite;
+ private final ToolBar fToolBar;
+
+ private boolean fMenuIsShown;
+ private boolean fEnabled;
+ private TreeViewer fDropDownViewer;
+ private Shell fShell;
+ private boolean isResizingProgrammatically;
+
+ public BreadcrumbItemDropDown(BreadcrumbItem parent, Composite composite) {
+ fParent = parent;
+ fParentComposite = composite;
+ fMenuIsShown = false;
+ fEnabled = true;
+
+ String showChildrenText = JFaceResources.getString("BreadcrumbItemDropDown_showDropDownMenu_action_toolTip"); //$NON-NLS-1$
+
+ fToolBar = new ToolBar(composite, SWT.FLAT);
+ fToolBar.setLayoutData(new GridData(SWT.END, SWT.CENTER, false, false));
+ fToolBar.getAccessible().addAccessibleListener(new AccessibleAdapter() {
+ @Override
+ public void getName(AccessibleEvent event) {
+ event.result = showChildrenText;
+ }
+ });
+ ToolBarManager manager = new ToolBarManager(fToolBar);
+
+ final Action showDropDownMenuAction = new Action(null, SWT.NONE) {
+ @Override
+ public void run() {
+ Shell shell = fParent.getDropDownShell();
+ if (shell != null)
+ return;
+
+ shell = fParent.getViewer().getDropDownShell();
+ if (shell != null)
+ shell.close();
+
+ showMenu();
+
+ fShell.setFocus();
+ }
+ };
+
+ showDropDownMenuAction.setImageDescriptor(new AccessibleArrowImage(isLTR()));
+ showDropDownMenuAction
+ .setToolTipText(showChildrenText);
+ manager.add(showDropDownMenuAction);
+
+ manager.update(true);
+ if (IS_MAC_WORKAROUND) {
+ manager.getControl().addMouseListener(new MouseAdapter() {
+ // see also BreadcrumbItemDetails#addElementListener(Control)
+ @Override
+ public void mouseDown(MouseEvent e) {
+ showDropDownMenuAction.run();
+ }
+ });
+ }
+ fToolBar.setData("org.eclipse.e4.ui.css.id", "BreadcrumbItemDropDownToolBar"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ /**
+ * Return the width of this element.
+ *
+ * @return the width of this element
+ */
+ public int getWidth() {
+ return fToolBar.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
+ }
+
+ /**
+ * Set whether the drop down menu is available.
+ *
+ * @param enabled true if available
+ */
+ public void setEnabled(boolean enabled) {
+ fEnabled = enabled;
+
+ fToolBar.setVisible(enabled);
+ }
+
+ /**
+ * Tells whether the menu is shown.
+ *
+ * @return true if the menu is open
+ */
+ public boolean isMenuShown() {
+ return fMenuIsShown;
+ }
+
+ /**
+ * Returns the shell used for the drop down menu if it is shown.
+ *
+ * @return the drop down shell or null
+ */
+ public Shell getDropDownShell() {
+ if (!isMenuShown())
+ return null;
+
+ return fShell;
+ }
+
+ /**
+ * Returns the drop down selection provider.
+ *
+ * @return the selection provider of the drop down if the menu is shown,
+ * {@code null} otherwise.
+ */
+ public ISelectionProvider getDropDownSelectionProvider() {
+ if (!fMenuIsShown)
+ return null;
+
+ return fDropDownViewer;
+ }
+
+ /**
+ * Opens the drop down menu.
+ */
+ public void showMenu() {
+
+ if (!fEnabled || fMenuIsShown)
+ return;
+
+ fMenuIsShown = true;
+
+ fShell = new Shell(fToolBar.getShell(), SWT.RESIZE | SWT.TOOL | SWT.ON_TOP);
+
+ GridLayout layout = new GridLayout(1, false);
+ layout.marginHeight = 0;
+ layout.marginWidth = 0;
+ fShell.setLayout(layout);
+
+ Composite composite = new Composite(fShell, SWT.NONE);
+ composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+ GridLayout gridLayout = new GridLayout(1, false);
+ gridLayout.marginHeight = 0;
+ gridLayout.marginWidth = 0;
+ composite.setLayout(gridLayout);
+
+ fDropDownViewer = fParent.getViewer().createDropDownViewer(composite, SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL);
+ fDropDownViewer.setUseHashlookup(true);
+
+ final Tree tree = (Tree) fDropDownViewer.getControl();
+ tree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+ Object input = fParent.getData();
+ fParent.getViewer().configureDropDownViewer(fDropDownViewer, input);
+ fDropDownViewer.setInput(input);
+
+ setShellBounds(fShell);
+
+ fShell.addControlListener(new ControlAdapter() {
+ /*
+ * @see
+ * org.eclipse.swt.events.ControlAdapter#controlResized(org.eclipse.swt.events.
+ * ControlEvent)
+ */
+ @Override
+ public void controlResized(ControlEvent e) {
+ if (isResizingProgrammatically)
+ return;
+
+ Point size = fShell.getSize();
+ getDialogSettings().put(DIALOG_HEIGHT, size.y);
+ }
+ });
+
+ fDropDownViewer.addOpenListener(event -> {
+
+ ISelection selection = event.getSelection();
+ if (!(selection instanceof IStructuredSelection))
+ return;
+
+ Object element = ((IStructuredSelection) selection).getFirstElement();
+ if (element == null)
+ return;
+
+ openElement(element);
+ });
+
+ tree.addMouseListener(new MouseListener() {
+ @Override
+ public void mouseUp(MouseEvent e) {
+
+ if (e.button != 1)
+ return;
+
+ if ((OpenStrategy.getOpenMethod() & OpenStrategy.SINGLE_CLICK) != 0)
+ return;
+
+ Item item = tree.getItem(new Point(e.x, e.y));
+ if (item == null)
+ return;
+
+ openElement(item.getData());
+ }
+
+ @Override
+ public void mouseDown(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseDoubleClick(MouseEvent e) {
+ }
+ });
+
+ tree.addMouseMoveListener(new MouseMoveListener() {
+ TreeItem fLastItem = null;
+
+ @Override
+ public void mouseMove(MouseEvent e) {
+ if (tree.equals(e.getSource())) {
+ TreeItem o = tree.getItem(new Point(e.x, e.y));
+ if ((o != null && !o.equals(fLastItem)) || (fLastItem != null && o == null)) {
+ boolean showHandPointer = false;
+ if (o != null) {
+ Object itemData = o.getData();
+ showHandPointer = itemData != null && fParent.getViewer().isElementOpenable(itemData);
+ }
+ tree.setCursor(showHandPointer ? tree.getDisplay().getSystemCursor(SWT.CURSOR_HAND) : null);
+ }
+ if (o != null) {
+ Rectangle clientArea = tree.getClientArea();
+ TreeItem currentItem = o;
+ if (!o.equals(fLastItem)) {
+ fLastItem = o;
+ tree.setSelection(new TreeItem[] { fLastItem });
+ } else if (e.y - clientArea.y < tree.getItemHeight() / 4) {
+ // Scroll up
+ if (currentItem.getParentItem() == null) {
+ int index = tree.indexOf(o);
+ if (index < 1)
+ return;
+
+ fLastItem = tree.getItem(index - 1);
+ tree.setSelection(new TreeItem[] { fLastItem });
+ } else {
+ Point p = tree.toDisplay(e.x, e.y);
+ Item item = fDropDownViewer.scrollUp(p.x, p.y);
+ if (item instanceof TreeItem) {
+ fLastItem = (TreeItem) item;
+ tree.setSelection(new TreeItem[] { fLastItem });
+ }
+ }
+ } else if (clientArea.y + clientArea.height - e.y < tree.getItemHeight() / 4) {
+ // Scroll down
+ if (currentItem.getParentItem() == null) {
+ int index = tree.indexOf(o);
+ if (index >= tree.getItemCount() - 1)
+ return;
+
+ fLastItem = tree.getItem(index + 1);
+ tree.setSelection(new TreeItem[] { fLastItem });
+ } else {
+ Point p = tree.toDisplay(e.x, e.y);
+ Item item = fDropDownViewer.scrollDown(p.x, p.y);
+ if (item instanceof TreeItem) {
+ fLastItem = (TreeItem) item;
+ tree.setSelection(new TreeItem[] { fLastItem });
+ }
+ }
+ }
+ } else {
+ fLastItem = null;
+ }
+ }
+ }
+ });
+
+ tree.addKeyListener(new KeyListener() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if (e.keyCode == SWT.ARROW_UP) {
+ TreeItem[] selection = tree.getSelection();
+ if (selection.length != 1)
+ return;
+
+ int selectionIndex = tree.indexOf(selection[0]);
+ if (selectionIndex != 0)
+ return;
+
+ fShell.close();
+ }
+ }
+
+ @Override
+ public void keyReleased(KeyEvent e) {
+ }
+ });
+
+ fDropDownViewer.addTreeListener(new ITreeViewerListener() {
+ @Override
+ public void treeCollapsed(TreeExpansionEvent event) {
+ }
+
+ @Override
+ public void treeExpanded(TreeExpansionEvent event) {
+ Shell shell = fShell;
+ tree.setRedraw(false);
+ shell.getDisplay().asyncExec(() -> {
+ if (shell.isDisposed() || tree.isDisposed())
+ return;
+
+ try {
+ resizeShell(shell);
+ } finally {
+ if (!tree.isDisposed())
+ tree.setRedraw(true);
+ }
+ });
+ }
+ });
+
+ int index = fParent.getViewer().getIndexOfItem(fParent);
+ if (index < fParent.getViewer().getItemCount() - 1) {
+ BreadcrumbItem childItem = fParent.getViewer().getItem(index + 1);
+ Object child = childItem.getData();
+
+ fDropDownViewer.setSelection(new StructuredSelection(child), true);
+
+ TreeItem[] selection = tree.getSelection();
+ if (selection.length > 0) {
+ tree.setTopItem(selection[0]);
+ }
+ }
+
+ fShell.setVisible(true);
+ installCloser(fShell);
+ }
+
+ private void openElement(Object data) {
+ if (data == null)
+ return;
+
+ // This might or might not open an editor
+ fParent.getViewer().fireMenuSelection(data);
+
+ boolean treeHasFocus = !fShell.isDisposed() && fDropDownViewer.getTree().isFocusControl();
+
+ if (fShell.isDisposed())
+ return;
+
+ if (!treeHasFocus) {
+ fShell.close();
+ return;
+ }
+
+ toggleExpansionState(data);
+ }
+
+ private void toggleExpansionState(Object element) {
+ Tree tree = fDropDownViewer.getTree();
+ if (fDropDownViewer.getExpandedState(element))
+ fDropDownViewer.collapseToLevel(element, 1);
+ else {
+ tree.setRedraw(false);
+ try {
+ fDropDownViewer.expandToLevel(element, 1);
+ resizeShell(fShell);
+ } finally {
+ tree.setRedraw(true);
+ }
+ }
+ }
+
+ /**
+ * The closer closes the given shell when the focus is lost.
+ *
+ * @param shell the shell to install the closer to
+ */
+ private void installCloser(final Shell shell) {
+ final Listener focusListener = event -> {
+ Widget focusElement = event.widget;
+ boolean isFocusBreadcrumbTreeFocusWidget = focusElement == shell
+ || focusElement instanceof Tree && ((Tree) focusElement).getShell() == shell;
+ boolean isFocusWidgetParentShell = focusElement instanceof Control
+ && ((Control) focusElement).getShell().getParent() == shell;
+
+ switch (event.type) {
+ case SWT.FocusIn:
+
+ if (!isFocusBreadcrumbTreeFocusWidget && !isFocusWidgetParentShell) {
+ shell.close();
+ }
+ break;
+
+ case SWT.FocusOut:
+
+ if (event.display.getActiveShell() == null) {
+ shell.close();
+ }
+ break;
+
+ default:
+ Assert.isTrue(false);
+ }
+ };
+
+ final Display display = shell.getDisplay();
+ display.addFilter(SWT.FocusIn, focusListener);
+ display.addFilter(SWT.FocusOut, focusListener);
+
+ final ControlListener controlListener = new ControlListener() {
+ @Override
+ public void controlMoved(ControlEvent e) {
+ shell.close();
+ }
+
+ @Override
+ public void controlResized(ControlEvent e) {
+ shell.close();
+ }
+ };
+ fToolBar.getShell().addControlListener(controlListener);
+
+ shell.addDisposeListener(e -> {
+
+ display.removeFilter(SWT.FocusIn, focusListener);
+ display.removeFilter(SWT.FocusOut, focusListener);
+
+ if (!fToolBar.isDisposed()) {
+ fToolBar.getShell().removeControlListener(controlListener);
+ }
+
+ // The shell may be disposed directly without receiving shellClosed.
+ fMenuIsShown = false;
+ fDropDownViewer = null;
+ });
+ shell.addShellListener(new ShellListener() {
+ @Override
+ public void shellActivated(ShellEvent e) {
+ }
+
+ @Override
+ public void shellClosed(ShellEvent e) {
+
+ if (!fMenuIsShown)
+ return;
+
+ fMenuIsShown = false;
+ fDropDownViewer = null;
+ }
+
+ @Override
+ public void shellDeactivated(ShellEvent e) {
+ }
+
+ @Override
+ public void shellDeiconified(ShellEvent e) {
+ }
+
+ @Override
+ public void shellIconified(ShellEvent e) {
+ }
+ });
+ }
+
+ private IDialogSettings getDialogSettings() {
+ IDialogSettings viewerSettings = fParent.getViewer().getDialogSettings();
+ IDialogSettings settings = viewerSettings.getSection(DIALOG_SETTINGS);
+ if (settings == null)
+ settings = viewerSettings.addNewSection(DIALOG_SETTINGS);
+ return settings;
+ }
+
+ private int getMaxHeight() {
+ try {
+ return getDialogSettings().getInt(DIALOG_HEIGHT);
+ } catch (NumberFormatException e) {
+ return DROP_DOWN_DEFAULT_MAX_HEIGHT;
+ }
+ }
+
+ /**
+ * Calculates a useful size for the given shell.
+ *
+ * @param shell the shell to calculate the size for.
+ */
+ private void setShellBounds(Shell shell) {
+ Rectangle rect = fParentComposite.getBounds();
+ Rectangle toolbarBounds = fToolBar.getBounds();
+
+ shell.pack();
+ Point size = shell.getSize();
+ int height = Math.max(Math.min(size.y, getMaxHeight()), DROP_DOWN_DEFAULT_MIN_HEIGHT);
+ int width = Math.max(Math.min(size.x, DROP_DOWN_MAX_WIDTH), DROP_DOWN_MIN_WIDTH);
+
+ int imageBoundsX = 0;
+ if (fDropDownViewer.getTree().getItemCount() > 0) {
+ TreeItem item = fDropDownViewer.getTree().getItem(0);
+ imageBoundsX = item.getImageBounds(0).x;
+ }
+
+ Rectangle trim = fShell.computeTrim(0, 0, width, height);
+ int x = toolbarBounds.x + toolbarBounds.width + 2 + trim.x - imageBoundsX;
+ if (!isLTR())
+ x += width;
+
+ // toDisplay expects coordinates relative to fParentComposite.
+ Point pt = new Point(x, rect.height);
+ pt = fParentComposite.toDisplay(pt);
+
+ Rectangle monitor = Util.getClosestMonitor(shell.getDisplay(), pt).getClientArea();
+ int overlap = (pt.x + width) - (monitor.x + monitor.width);
+ if (overlap > 0)
+ pt.x -= overlap;
+ if (pt.x < monitor.x)
+ pt.x = monitor.x;
+
+ shell.setLocation(pt);
+ shell.setSize(width, height);
+ }
+
+ /**
+ * Set the size of the given shell such that more content can be shown. The
+ * shell size does not exceed a user-configurable maximum.
+ *
+ * @param shell the shell to resize
+ */
+ private void resizeShell(final Shell shell) {
+ Point size = shell.getSize();
+ int currentWidth = size.x;
+ int currentHeight = size.y;
+
+ int maxHeight = getMaxHeight();
+
+ if (currentHeight >= maxHeight && currentWidth >= DROP_DOWN_MAX_WIDTH)
+ return;
+
+ Point preferedSize = shell.computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
+
+ int newWidth;
+ if (currentWidth >= DROP_DOWN_MAX_WIDTH) {
+ newWidth = currentWidth;
+ } else {
+ newWidth = Math.min(Math.max(preferedSize.x, currentWidth), DROP_DOWN_MAX_WIDTH);
+ }
+ int newHeight;
+ if (currentHeight >= maxHeight) {
+ newHeight = currentHeight;
+ } else {
+ newHeight = Math.min(Math.max(preferedSize.y, currentHeight), maxHeight);
+ }
+
+ if (newHeight != currentHeight || newWidth != currentWidth) {
+ shell.setRedraw(false);
+ try {
+ isResizingProgrammatically = true;
+ shell.setSize(newWidth, newHeight);
+ if (!isLTR()) {
+ Point location = shell.getLocation();
+ shell.setLocation(location.x - (newWidth - currentWidth), location.y);
+ }
+ } finally {
+ isResizingProgrammatically = false;
+ shell.setRedraw(true);
+ }
+ }
+ }
+
+ /**
+ * Tells whether this the breadcrumb is in LTR or RTL mode.
+ *
+ * @return true if the breadcrumb in left-to-right mode,
+ * false otherwise
+ */
+ private boolean isLTR() {
+ return (fParentComposite.getStyle() & SWT.RIGHT_TO_LEFT) == 0;
+ }
+}
diff --git a/bundles/org.eclipse.jface/src/org/eclipse/jface/viewers/BreadcrumbViewer.java b/bundles/org.eclipse.jface/src/org/eclipse/jface/viewers/BreadcrumbViewer.java
new file mode 100644
index 00000000000..ddec7e7a139
--- /dev/null
+++ b/bundles/org.eclipse.jface/src/org/eclipse/jface/viewers/BreadcrumbViewer.java
@@ -0,0 +1,825 @@
+/*******************************************************************************
+ * Copyright (c) 2008, 2011 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ * Fabio Zadrozny - Bug 465666
+ *******************************************************************************/
+package org.eclipse.jface.viewers;
+
+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.dialogs.DialogSettings;
+import org.eclipse.jface.dialogs.IDialogSettings;
+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.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
+ * ITreeContentProvider interface.
+ *
+ *
+ * Label providers for breadcrumb viewers must either implement
+ * ILabelProvider or extend CellLabelProvider.
+ *
+ *
+ * @since 3.41
+ */
+public abstract class BreadcrumbViewer extends StructuredViewer {
+
+ private static final boolean IS_GTK = "gtk".equals(SWT.getPlatform()); //$NON-NLS-1$
+
+ private final Composite fContainer;
+ private final ArrayList fBreadcrumbItems;
+ private final ListenerList fMenuListeners;
+
+ private Image fGradientBackground;
+ private BreadcrumbItem fSelectedItem;
+ private ILabelProvider fToolTipLabelProvider;
+ private final IDialogSettings fDialogSettings = new DialogSettings("BreadcrumbViewer"); //$NON-NLS-1$
+
+ private final boolean fVertical;
+
+ /**
+ * Create a new BreadcrumbViewer.
+ *
+ * Style is one of:
+ *
+ *
SWT.NONE
+ *
SWT.VERTICAL
+ *
SWT.HORIZONTAL
+ *
+ *
+ * @param parent the container for the viewer
+ * @param style the style flag used for this viewer
+ */
+ public BreadcrumbViewer(Composite parent, int style) {
+ fBreadcrumbItems = new ArrayList<>();
+ fMenuListeners = new ListenerList<>();
+
+ fVertical = (SWT.VERTICAL & style) != 0;
+
+ 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);
+ fContainer.setData("org.eclipse.e4.ui.css.id", "BreadcrumbComposite"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ hookControl(fContainer);
+
+ int columns = 1000;
+ if (fVertical) {
+ columns = 1;
+ }
+
+ 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 -> refresh());
+ }
+
+ /**
+ * 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 viewer the viewer to configure
+ * @param input the input for the viewer
+ */
+ protected abstract void configureDropDownViewer(TreeViewer viewer, Object input);
+
+ /**
+ * The tool tip to use for the tool tip labels. null if the viewers
+ * label provider should be used.
+ *
+ * @param toolTipLabelProvider the label provider for the tool tips or
+ * null
+ */
+ public void setToolTipLabelProvider(ILabelProvider toolTipLabelProvider) {
+ fToolTipLabelProvider = toolTipLabelProvider;
+
+ for (BreadcrumbItem item : fBreadcrumbItems) {
+ item.refreshToolTip();
+ }
+ }
+
+ ILabelProvider getToolTipLabelProvider() {
+ return fToolTipLabelProvider;
+ }
+
+ /*
+ * @see org.eclipse.jface.viewers.Viewer#getControl()
+ */
+ @Override
+ public Control getControl() {
+ return fContainer;
+ }
+
+ /*
+ * @see org.eclipse.jface.viewers.StructuredViewer#getRoot()
+ */
+ @Override
+ protected Object getRoot() {
+ if (fBreadcrumbItems.isEmpty())
+ return null;
+
+ return fBreadcrumbItems.get(0).getData();
+ }
+
+ /*
+ * @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.isEmpty())
+ return;
+
+ BreadcrumbItem item = fBreadcrumbItems.get(fBreadcrumbItems.size() - 1);
+ if (item.getData() == null) {
+ if (fBreadcrumbItems.size() < 2)
+ return;
+
+ item = fBreadcrumbItems.get(fBreadcrumbItems.size() - 2);
+ }
+ item.setFocus(true);
+ }
+ }
+
+ /**
+ * @return true if any of the items in the viewer is expanded
+ */
+ public boolean isDropDownOpen() {
+ for (BreadcrumbItem item : fBreadcrumbItems) {
+ 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 (BreadcrumbItem item : fBreadcrumbItems) {
+ if (item.isMenuShown())
+ return item.getDropDownShell();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the selection provider which provides the selection of the drop down
+ * currently opened or null if no drop down is open at the moment.
+ *
+ * @return the selection provider of the open drop down or null
+ */
+ public ISelectionProvider getDropDownSelectionProvider() {
+ for (BreadcrumbItem item : fBreadcrumbItems) {
+ if (item.isMenuShown()) {
+ return item.getDropDownSelectionProvider();
+ }
+ }
+
+ 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 ITreeContentProvider);
+ }
+
+ /*
+ * @see org.eclipse.jface.viewers.Viewer#inputChanged(java.lang.Object,
+ * java.lang.Object)
+ */
+ @Override
+ protected void inputChanged(Object fInput, Object oldInput) {
+ if (fContainer.isDisposed())
+ return;
+
+ disableRedraw();
+ try {
+ rebuildItemChain(fInput);
+ } finally {
+ enableRedraw();
+ }
+ }
+
+ private void rebuildItemChain(Object input) {
+ if (fBreadcrumbItems.size() > 0) {
+ BreadcrumbItem last = fBreadcrumbItems.get(fBreadcrumbItems.size() - 1);
+ last.setIsLastItem(false);
+ }
+
+ int lastIndex = buildItemChain(input);
+
+ if (lastIndex > 0) {
+ BreadcrumbItem last = fBreadcrumbItems.get(lastIndex - 1);
+ last.setIsLastItem(true);
+ }
+
+ while (lastIndex < fBreadcrumbItems.size()) {
+ BreadcrumbItem item = fBreadcrumbItems.remove(fBreadcrumbItems.size() - 1);
+ if (item == fSelectedItem) {
+ selectItem(null);
+ }
+ if (item.getData() != null)
+ unmapElement(item.getData(), item);
+ item.dispose();
+ }
+
+ updateSize();
+ fContainer.layout(true, true);
+ }
+
+ /*
+ * @see
+ * org.eclipse.jface.viewers.StructuredViewer#doFindInputItem(java.lang.Object)
+ */
+ @Override
+ protected Widget doFindInputItem(Object element) {
+ if (element == null)
+ return null;
+
+ if (equals(element, 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 (BreadcrumbItem item : fBreadcrumbItems) {
+ if (equals(element, 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) {
+ 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);
+ }
+
+ BreadcrumbViewerRow row = new BreadcrumbViewerRow(this, item);
+ ViewerCell cell = row.getCell(0);
+
+ IBaseLabelProvider labelProvider = getLabelProvider();
+ if (labelProvider instanceof CellLabelProvider cellLabelProvider) {
+ cellLabelProvider.update(cell);
+ } else {
+ ILabelProvider simpleLabelProvider = (ILabelProvider) labelProvider;
+ cell.setText(simpleLabelProvider.getText(element));
+ cell.setImage(simpleLabelProvider.getImage(element));
+ }
+
+ item.refreshArrow();
+ item.refreshToolTip();
+
+ updateSize();
+ fContainer.layout(true, true);
+ }
+ }
+
+ /*
+ * @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