/******************************************************************************* * Copyright (c) 2008, 2017 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 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(); } }); } } /** * 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); } } }