| /******************************************************************************* |
| * Copyright (c) 2008, 2017 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 |
| * Pawel Piech (Wind River) - adapted breadcrumb for use in Debug view (Bug 252677) |
| *******************************************************************************/ |
| package org.eclipse.debug.internal.ui.viewers.breadcrumb; |
| |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.debug.internal.ui.DebugUIPlugin; |
| 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.util.Geometry; |
| import org.eclipse.jface.viewers.ISelection; |
| import org.eclipse.jface.viewers.TreePath; |
| 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.DisposeEvent; |
| import org.eclipse.swt.events.DisposeListener; |
| import org.eclipse.swt.events.MouseAdapter; |
| import org.eclipse.swt.events.MouseEvent; |
| 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.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.Event; |
| import org.eclipse.swt.widgets.Listener; |
| import org.eclipse.swt.widgets.Monitor; |
| import org.eclipse.swt.widgets.Shell; |
| import org.eclipse.swt.widgets.ToolBar; |
| import org.eclipse.swt.widgets.Widget; |
| |
| |
| /** |
| * The part of the breadcrumb item with the drop down menu. |
| * |
| * @since 3.5 |
| */ |
| class BreadcrumbItemDropDown implements IBreadcrumbDropDownSite { |
| |
| 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 <code>ltr</code> 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; |
| } |
| |
| /* |
| * @see org.eclipse.jface.resource.CompositeImageDescriptor#drawCompositeImage(int, int) |
| */ |
| @Override |
| protected void drawCompositeImage(int width, int height) { |
| Display display= fParentComposite.getDisplay(); |
| ImageDataProvider imageProvider = zoom -> { |
| Image image = new Image(display, ARROW_SIZE, ARROW_SIZE * 2); |
| |
| GC gc = new GC(image, fLTR ? SWT.LEFT_TO_RIGHT : SWT.RIGHT_TO_LEFT); |
| 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 }); |
| gc.dispose(); |
| triangleColor.dispose(); |
| |
| ImageData imageData = image.getImageData(zoom); |
| image.dispose(); |
| int zoomedArrowSize = ARROW_SIZE * zoom / 100; |
| for (int y1 = 0; y1 < zoomedArrowSize; y1++) { |
| for (int x1 = 0; x1 <= y1; x1++) { |
| imageData.setAlpha(fLTR ? x1 : zoomedArrowSize - x1 - 1, y1, 255); |
| } |
| } |
| for (int y2 = 0; y2 < zoomedArrowSize; y2++) { |
| for (int x2 = 0; x2 <= y2; x2++) { |
| imageData.setAlpha(fLTR ? x2 : zoomedArrowSize - x2 - 1, zoomedArrowSize * 2 - y2 - 1, 255); |
| } |
| } |
| return imageData; |
| }; |
| drawImage(imageProvider, (width / 2) - (ARROW_SIZE / 2), (height / 2) - ARROW_SIZE); |
| |
| } |
| |
| /* |
| * @see org.eclipse.jface.resource.CompositeImageDescriptor#getSize() |
| */ |
| @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= BreadcrumbViewer.blend(rgb2, rgb1, ratio); |
| |
| return new Color(display, blend); |
| } |
| } |
| |
| // Workaround for bug 258196: set the minimum size to 500 because on Linux |
| // the size is not adjusted correctly in a virtual tree. |
| private static final int DROP_DOWN_MIN_WIDTH= 500; |
| private static final int DROP_DOWN_MAX_WIDTH= 501; |
| |
| private static final int DROP_DOWN_DEFAULT_MIN_HEIGHT= 100; |
| private static final int DROP_DOWN_DEFAULT_MAX_HEIGHT= 500; |
| |
| private static final String DIALOG_SETTINGS= "BreadcrumbItemDropDown"; //$NON-NLS-1$ |
| private static final String DIALOG_HEIGHT= "height"; //$NON-NLS-1$ |
| private static final String DIALOG_WIDTH= "width"; //$NON-NLS-1$ |
| |
| private final BreadcrumbItem fParent; |
| private final Composite fParentComposite; |
| private final ToolBar fToolBar; |
| |
| private boolean fMenuIsShown; |
| private boolean fEnabled; |
| private Shell fShell; |
| private boolean fIsResizingProgrammatically; |
| private int fCurrentWidth = -1; |
| private int fCurrentHeight = -1; |
| |
| |
| public BreadcrumbItemDropDown(BreadcrumbItem parent, Composite composite) { |
| fParent= parent; |
| fParentComposite= composite; |
| fMenuIsShown= false; |
| fEnabled= true; |
| |
| 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 e) { |
| e.result= BreadcrumbMessages.BreadcrumbItemDropDown_showDropDownMenu_action_toolTip; |
| } |
| }); |
| 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.isDisposed()) { |
| shell.close(); |
| } |
| |
| showMenu(); |
| |
| fShell.setFocus(); |
| } |
| }; |
| |
| showDropDownMenuAction.setImageDescriptor(new AccessibleArrowImage(isLeft())); |
| showDropDownMenuAction.setToolTipText(BreadcrumbMessages.BreadcrumbItemDropDown_showDropDownMenu_action_toolTip); |
| 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", "DebugBreadcrumbItemDropDownToolBar"); //$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 <code>null</code> |
| */ |
| public Shell getDropDownShell() { |
| if (!isMenuShown()) { |
| return null; |
| } |
| |
| return fShell; |
| } |
| |
| /** |
| * Opens the drop down menu. |
| */ |
| public void showMenu() { |
| if (DebugUIPlugin.DEBUG_BREADCRUMB) { |
| DebugUIPlugin.trace("BreadcrumbItemDropDown.showMenu()"); //$NON-NLS-1$ |
| } |
| |
| if (!fEnabled || fMenuIsShown) { |
| return; |
| } |
| |
| fMenuIsShown= true; |
| |
| fShell= new Shell(fToolBar.getShell(), SWT.RESIZE | SWT.TOOL | SWT.ON_TOP); |
| if (DebugUIPlugin.DEBUG_BREADCRUMB) { |
| DebugUIPlugin.trace(" creating new shell"); //$NON-NLS-1$ |
| } |
| |
| fShell.addControlListener(new ControlAdapter() { |
| /* |
| * @see org.eclipse.swt.events.ControlAdapter#controlResized(org.eclipse.swt.events.ControlEvent) |
| */ |
| @Override |
| public void controlResized(ControlEvent e) { |
| if (fIsResizingProgrammatically) { |
| return; |
| } |
| |
| Point size= fShell.getSize(); |
| fCurrentWidth = size.x; |
| fCurrentHeight = size.y; |
| getDialogSettings().put(DIALOG_WIDTH, size.x); |
| getDialogSettings().put(DIALOG_HEIGHT, size.y); |
| } |
| }); |
| |
| 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); |
| |
| TreePath path= fParent.getPath(); |
| |
| Control control = fParent.getViewer().createDropDown(composite, this, path); |
| |
| control.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); |
| |
| setShellBounds(fShell); |
| fShell.setVisible(true); |
| installCloser(fShell); |
| } |
| |
| /** |
| * 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= new Listener() { |
| @Override |
| public void handleEvent(Event event) { |
| Widget focusElement= event.widget; |
| boolean isFocusBreadcrumbTreeFocusWidget= focusElement == shell || focusElement instanceof Control && ((Control)focusElement).getShell() == shell; |
| boolean isFocusWidgetParentShell= focusElement instanceof Control && ((Control)focusElement).getShell().getParent() == shell; |
| |
| switch (event.type) { |
| case SWT.FocusIn: |
| if (DebugUIPlugin.DEBUG_BREADCRUMB) { |
| DebugUIPlugin.trace("focusIn - is breadcrumb tree: " + isFocusBreadcrumbTreeFocusWidget); //$NON-NLS-1$ |
| } |
| |
| if (!isFocusBreadcrumbTreeFocusWidget && !isFocusWidgetParentShell) { |
| if (DebugUIPlugin.DEBUG_BREADCRUMB) { |
| DebugUIPlugin.trace("==> closing shell since focus in other widget"); //$NON-NLS-1$ |
| } |
| shell.close(); |
| } |
| break; |
| |
| case SWT.FocusOut: |
| if (DebugUIPlugin.DEBUG_BREADCRUMB) { |
| DebugUIPlugin.trace("focusOut - is breadcrumb tree: " + isFocusBreadcrumbTreeFocusWidget); //$NON-NLS-1$ |
| } |
| if (event.display.getActiveShell() == null) { |
| if (DebugUIPlugin.DEBUG_BREADCRUMB) { |
| DebugUIPlugin.trace("==> closing shell since event.display.getActiveShell() != shell"); //$NON-NLS-1$ |
| } |
| 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) { |
| if (!shell.isDisposed()) { |
| shell.close(); |
| } |
| } |
| |
| @Override |
| public void controlResized(ControlEvent e) { |
| if (!shell.isDisposed()) { |
| shell.close(); |
| } |
| } |
| }; |
| fToolBar.getShell().addControlListener(controlListener); |
| |
| shell.addDisposeListener(new DisposeListener() { |
| @Override |
| public void widgetDisposed(DisposeEvent e) { |
| if (DebugUIPlugin.DEBUG_BREADCRUMB) { |
| DebugUIPlugin.trace("==> shell disposed"); //$NON-NLS-1$ |
| } |
| |
| display.removeFilter(SWT.FocusIn, focusListener); |
| display.removeFilter(SWT.FocusOut, focusListener); |
| |
| if (!fToolBar.isDisposed()) { |
| fToolBar.getShell().removeControlListener(controlListener); |
| } |
| } |
| }); |
| shell.addShellListener(new ShellListener() { |
| @Override |
| public void shellActivated(ShellEvent e) { |
| } |
| |
| @Override |
| public void shellClosed(ShellEvent e) { |
| if (DebugUIPlugin.DEBUG_BREADCRUMB) { |
| DebugUIPlugin.trace("==> shellClosed"); //$NON-NLS-1$ |
| } |
| if (!fMenuIsShown) { |
| return; |
| } |
| |
| fMenuIsShown= false; |
| } |
| |
| @Override |
| public void shellDeactivated(ShellEvent e) { |
| } |
| |
| @Override |
| public void shellDeiconified(ShellEvent e) { |
| } |
| |
| @Override |
| public void shellIconified(ShellEvent e) { |
| } |
| }); |
| } |
| |
| private IDialogSettings getDialogSettings() { |
| IDialogSettings javaSettings= DebugUIPlugin.getDefault().getDialogSettings(); |
| IDialogSettings settings= javaSettings.getSection(DIALOG_SETTINGS); |
| if (settings == null) { |
| settings= javaSettings.addNewSection(DIALOG_SETTINGS); |
| } |
| return settings; |
| } |
| |
| private int getMaxWidth() { |
| try { |
| return getDialogSettings().getInt(DIALOG_WIDTH); |
| } catch (NumberFormatException e) { |
| return DROP_DOWN_MAX_WIDTH; |
| } |
| } |
| |
| 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(); |
| |
| Point size = shell.computeSize(SWT.DEFAULT, SWT.DEFAULT, false); |
| int height= Math.max(Math.min(size.y, getMaxHeight()), DROP_DOWN_DEFAULT_MIN_HEIGHT); |
| int width= Math.max(getMaxWidth(), DROP_DOWN_MIN_WIDTH); |
| |
| int imageBoundsX= 0; |
| if (fParent.getImage() != null) { |
| imageBoundsX= fParent.getImage().getImageData().width; |
| } |
| |
| Rectangle trim= fShell.computeTrim(0, 0, width, height); |
| int x= toolbarBounds.x + toolbarBounds.width + 2 + trim.x - imageBoundsX; |
| if (!isLeft()) { |
| x+= width; |
| } |
| |
| int y = rect.y; |
| if (isTop()) { |
| y+= rect.height; |
| } else { |
| y-= height; |
| } |
| |
| Point pt= new Point(x, y); |
| pt= fParentComposite.toDisplay(pt); |
| |
| Rectangle monitor= 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); |
| fIsResizingProgrammatically= true; |
| try { |
| shell.setSize(width, height); |
| fCurrentWidth = width; |
| fCurrentHeight = height; |
| } finally { |
| fIsResizingProgrammatically= false; |
| } |
| } |
| |
| /** |
| * Returns the monitor whose client area contains the given point. If no monitor contains the |
| * point, returns the monitor that is closest to the point. |
| * <p> |
| * Copied from <code>org.eclipse.jface.window.Window.getClosestMonitor(Display, Point)</code> |
| * </p> |
| * |
| * @param display the display showing the monitors |
| * @param point point to find (display coordinates) |
| * @return the monitor closest to the given point |
| */ |
| private static Monitor getClosestMonitor(Display display, Point point) { |
| int closest= Integer.MAX_VALUE; |
| |
| Monitor[] monitors= display.getMonitors(); |
| Monitor result= monitors[0]; |
| |
| for (int i= 0; i < monitors.length; i++) { |
| Monitor current= monitors[i]; |
| |
| Rectangle clientArea= current.getClientArea(); |
| |
| if (clientArea.contains(point)) { |
| return current; |
| } |
| |
| int distance= Geometry.distanceSquared(Geometry.centerPoint(clientArea), point); |
| if (distance < closest) { |
| closest= distance; |
| result= current; |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * 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) { |
| int maxHeight= getMaxHeight(); |
| int maxWidth = getMaxWidth(); |
| |
| if (fCurrentHeight >= maxHeight && fCurrentWidth >= maxWidth) { |
| return; |
| } |
| |
| Point preferedSize= shell.computeSize(SWT.DEFAULT, SWT.DEFAULT, true); |
| |
| int newWidth; |
| if (fCurrentWidth >= DROP_DOWN_MAX_WIDTH) { |
| newWidth= fCurrentWidth; |
| } else { |
| // Workaround for bug 319612: Do not resize width below the |
| // DROP_DOWN_MIN_WIDTH. This can happen because the Shell.getSize() |
| // is incorrectly small on Linux. |
| newWidth= Math.min(Math.max(Math.max(preferedSize.x, fCurrentWidth), DROP_DOWN_MIN_WIDTH), maxWidth); |
| } |
| int newHeight; |
| if (fCurrentHeight >= maxHeight) { |
| newHeight= fCurrentHeight; |
| } else { |
| newHeight= Math.min(Math.max(preferedSize.y, fCurrentHeight), maxHeight); |
| } |
| |
| if (newHeight != fCurrentHeight || newWidth != fCurrentWidth) { |
| shell.setRedraw(false); |
| try { |
| fIsResizingProgrammatically= true; |
| shell.setSize(newWidth, newHeight); |
| fCurrentWidth = newWidth; |
| fCurrentHeight = newHeight; |
| |
| Point location = shell.getLocation(); |
| Point newLocation = location; |
| if (!isLeft()) { |
| newLocation = new Point(newLocation.x - (newWidth - fCurrentWidth), newLocation.y); |
| } |
| if (!isTop()) { |
| newLocation = new Point(newLocation.x, newLocation.y - (newHeight - fCurrentHeight)); |
| } |
| if (!location.equals(newLocation)) { |
| shell.setLocation(newLocation.x, newLocation.y); |
| } |
| } finally { |
| fIsResizingProgrammatically= false; |
| shell.setRedraw(true); |
| } |
| } |
| } |
| |
| /** |
| * Tells whether this the breadcrumb is in LTR mode or RTL mode. Or whether the breadcrumb |
| * is on the right-side status coolbar, which has the same effect on layout. |
| * |
| * @return <code>true</code> if the breadcrumb in left-to-right mode, <code>false</code> |
| * otherwise |
| */ |
| private boolean isLeft() { |
| return (fParentComposite.getStyle() & SWT.RIGHT_TO_LEFT) == 0 && |
| (fParent.getViewer().getStyle() & SWT.RIGHT) == 0; |
| } |
| |
| /** |
| * Tells whether this the breadcrumb is in LTR mode or RTL mode. Or whether the breadcrumb |
| * is on the right-side status coolbar, which has the same effect on layout. |
| * |
| * @return <code>true</code> if the breadcrumb in left-to-right mode, <code>false</code> |
| * otherwise |
| */ |
| private boolean isTop() { |
| return (fParent.getViewer().getStyle() & SWT.BOTTOM) == 0; |
| } |
| |
| @Override |
| public void close() { |
| if (fShell != null && !fShell.isDisposed()) { |
| fShell.close(); |
| } |
| } |
| |
| @Override |
| public void notifySelection(ISelection selection) { |
| fParent.getViewer().fireMenuSelection(selection); |
| } |
| |
| @Override |
| public void updateSize() { |
| if (fShell != null && !fShell.isDisposed()) { |
| resizeShell(fShell); |
| } |
| } |
| } |
| |