/******************************************************************************* * Copyright (c) 2009, 2012 SpringSource, a divison of VMware, Inc. * 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: * SpringSource, a division of VMware, Inc. - initial API and implementation *******************************************************************************/ package org.eclipse.virgo.ide.ui.editors.text; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.ListenerList; import org.eclipse.jface.action.ToolBarManager; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.text.IInformationControl; import org.eclipse.jface.text.IInformationControlCreator; import org.eclipse.jface.text.IInformationControlExtension; import org.eclipse.jface.text.IInformationControlExtension2; import org.eclipse.jface.text.IInformationControlExtension3; import org.eclipse.jface.text.IInformationControlExtension4; import org.eclipse.jface.util.Geometry; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Cursor; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Canvas; 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.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Slider; import org.eclipse.swt.widgets.ToolBar; /** * An abstract information control that can show content inside a shell. The information control can be created in two * styles: * <ul> * <li>non-resizable tooltip with optional status</li> * <li>resizable tooltip with optional tool bar</li> * </ul> * Additionally it can present either a status line containing a status text or a toolbar containing toolbar buttons. * <p> * Subclasses must either override {@link IInformationControl#setInformation(String)} or implement * {@link IInformationControlExtension2}. They should also extend {@link #computeTrim()} if they create a content area * with additional trim (e.g. scrollbars) and override {@link #getInformationPresenterControlCreator()}. * </p> * * @author Christian Dupuis * @since 3.4 */ public abstract class JFaceAbstractInformationControl implements IInformationControl, IInformationControlExtension, IInformationControlExtension3, IInformationControlExtension4, JFaceIInformationControlExtension5 { /** The information control's shell. */ private final Shell fShell; /** Composite containing the content created by subclasses. */ private final Composite fContentComposite; /** Whether the information control is resizable. */ private final boolean fResizable; /** * Composite containing the status line content or <code>null</code> if none. */ private Composite fStatusComposite; /** Separator between content and status line or <code>null</code> if none. */ private Label fSeparator; /** Label in the status line or <code>null</code> if none. */ private Label fStatusLabel; /** The toolbar manager used by the toolbar or <code>null</code> if none. */ private final ToolBarManager fToolBarManager; /** Status line toolbar or <code>null</code> if none. */ private ToolBar fToolBar; /** Listener for shell activation and deactivation. */ private Listener fShellListener; /** All focus listeners registered to this information control. */ private final ListenerList fFocusListeners = new ListenerList(ListenerList.IDENTITY); /** * Size constraints, x is the maxWidth and y is the maxHeight, or <code>null</code> if not set. */ private Point fSizeConstraints; /** The size of the resize handle if already set, -1 otherwise */ private int fResizeHandleSize; /** * Creates an abstract information control with the given shell as parent. The control will not be resizable and * optionally show a status line with the given status field text. * <p> * <em>Important: Subclasses are required to call {@link #create()} at the end of their constructor.</em> * </p> * * @param parentShell the parent of this control's shell * @param statusFieldText the text to be used in the status field or <code>null</code> to hide the status field */ public JFaceAbstractInformationControl(Shell parentShell, String statusFieldText) { this(parentShell, SWT.TOOL | SWT.ON_TOP, statusFieldText, null); } /** * Creates an abstract information control with the given shell as parent. The control will be resizable and * optionally show a tool bar managed by the given tool bar manager. * <p> * <em>Important: Subclasses are required to call {@link #create()} at the end of their constructor.</em> * </p> * * @param parentShell the parent of this control's shell * @param toolBarManager the manager or <code>null</code> if toolbar is not desired */ public JFaceAbstractInformationControl(Shell parentShell, ToolBarManager toolBarManager) { this(parentShell, SWT.TOOL | SWT.ON_TOP | SWT.RESIZE, null, toolBarManager); } /** * Creates an abstract information control with the given shell as parent. * <p> * <em>Important: Subclasses are required to call {@link #create()} at the end of their constructor.</em> * </p> * * @param parentShell the parent of this control's shell * @param isResizable <code>true</code> if the control should be resizable */ public JFaceAbstractInformationControl(Shell parentShell, boolean isResizable) { this(parentShell, SWT.TOOL | SWT.ON_TOP | (isResizable ? SWT.RESIZE : 0), null, null); } /** * Creates an abstract information control with the given shell as parent. The given shell style is used for the * shell (NO_TRIM will be removed to make sure there's a border). * <p> * The control will optionally show either a status line or a tool bar. At most one of <code>toolBarManager</code> * or <code>statusFieldText</code> can be non-null. * </p> * <p> * <strong>Important:</strong>: Subclasses are required to call {@link #create()} at the end of their constructor. * </p> * * @param parentShell the parent of this control's shell * @param shellStyle style of this control's shell * @param statusFieldText the text to be used in the status field or <code>null</code> to hide the status field * @param toolBarManager the manager or <code>null</code> if toolbar is not desired * @deprecated clients should use one of the public constructors */ @Deprecated JFaceAbstractInformationControl(Shell parentShell, int shellStyle, final String statusFieldText, final ToolBarManager toolBarManager) { Assert.isTrue(statusFieldText == null || toolBarManager == null); this.fResizeHandleSize = -1; this.fToolBarManager = toolBarManager; if ((shellStyle & SWT.NO_TRIM) != 0) { shellStyle &= ~(SWT.NO_TRIM | SWT.SHELL_TRIM); // make sure we get // the OS border but // no other trims } this.fResizable = (shellStyle & SWT.RESIZE) != 0; // on GTK, Shell removes // SWT.RESIZE if SWT.ON_TOP // is set this.fShell = new Shell(parentShell, shellStyle); Display display = this.fShell.getDisplay(); Color foreground = display.getSystemColor(SWT.COLOR_INFO_FOREGROUND); Color background = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND); setColor(this.fShell, foreground, background); GridLayout layout = new GridLayout(1, false); layout.marginHeight = 0; layout.marginWidth = 0; layout.verticalSpacing = 0; this.fShell.setLayout(layout); this.fContentComposite = new Composite(this.fShell, SWT.NONE); this.fContentComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); this.fContentComposite.setLayout(new FillLayout()); setColor(this.fContentComposite, foreground, background); createStatusComposite(statusFieldText, toolBarManager, foreground, background); } private void createStatusComposite(final String statusFieldText, final ToolBarManager toolBarManager, Color foreground, Color background) { if (toolBarManager == null && statusFieldText == null) { return; } this.fStatusComposite = new Composite(this.fShell, SWT.NONE); GridData gridData = new GridData(SWT.FILL, SWT.BOTTOM, true, false); this.fStatusComposite.setLayoutData(gridData); GridLayout statusLayout = new GridLayout(1, false); statusLayout.marginHeight = 0; statusLayout.marginWidth = 0; statusLayout.verticalSpacing = 1; this.fStatusComposite.setLayout(statusLayout); this.fSeparator = new Label(this.fStatusComposite, SWT.SEPARATOR | SWT.HORIZONTAL); this.fSeparator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); if (statusFieldText != null) { createStatusLabel(statusFieldText, foreground, background); } else { createToolBar(toolBarManager); } } private void createStatusLabel(final String statusFieldText, Color foreground, Color background) { this.fStatusLabel = new Label(this.fStatusComposite, SWT.RIGHT); this.fStatusLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); this.fStatusLabel.setText(statusFieldText); FontData[] fontDatas = JFaceResources.getDialogFont().getFontData(); for (FontData element : fontDatas) { element.setHeight(element.getHeight() * 9 / 10); } this.fStatusLabel.setFont(new Font(this.fStatusLabel.getDisplay(), fontDatas)); this.fStatusLabel.setForeground(this.fStatusLabel.getDisplay().getSystemColor(SWT.COLOR_WIDGET_DARK_SHADOW)); this.fStatusLabel.setBackground(background); setColor(this.fStatusComposite, foreground, background); } private void createToolBar(ToolBarManager toolBarManager) { final Composite bars = new Composite(this.fStatusComposite, SWT.NONE); bars.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false)); GridLayout layout = new GridLayout(3, false); layout.marginHeight = 0; layout.marginWidth = 0; layout.horizontalSpacing = 0; layout.verticalSpacing = 0; bars.setLayout(layout); this.fToolBar = toolBarManager.createControl(bars); GridData gd = new GridData(SWT.BEGINNING, SWT.BEGINNING, false, false); this.fToolBar.setLayoutData(gd); Composite spacer = new Composite(bars, SWT.NONE); gd = new GridData(SWT.FILL, SWT.FILL, true, true); gd.widthHint = 0; gd.heightHint = 0; spacer.setLayoutData(gd); addMoveSupport(spacer); addResizeSupportIfNecessary(bars); } private void addResizeSupportIfNecessary(final Composite bars) { // XXX: workarounds for // - https://bugs.eclipse.org/bugs/show_bug.cgi?id=219139 : API to add // resize grip / grow box in lower right corner of shell // - https://bugs.eclipse.org/bugs/show_bug.cgi?id=23980 : platform // specific shell resize behavior String platform = SWT.getPlatform(); final boolean isWin = platform.equals("win32"); //$NON-NLS-1$ if (!isWin && !platform.equals("gtk")) { return; } final Canvas resizer = new Canvas(bars, SWT.NONE); int size = getResizeHandleSize(bars); GridData data = new GridData(SWT.END, SWT.END, false, true); data.widthHint = size; data.heightHint = size; resizer.setLayoutData(data); resizer.addPaintListener(new PaintListener() { public void paintControl(PaintEvent e) { Point s = resizer.getSize(); int x = s.x - 2; int y = s.y - 2; int min = Math.min(x, y); if (isWin) { // draw dots e.gc.setBackground(resizer.getDisplay().getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW)); int end = min - 1; for (int i = 0; i <= 2; i++) { for (int j = 0; j <= 2 - i; j++) { e.gc.fillRectangle(end - 4 * i, end - 4 * j, 2, 2); } } end--; e.gc.setBackground(resizer.getDisplay().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW)); for (int i = 0; i <= 2; i++) { for (int j = 0; j <= 2 - i; j++) { e.gc.fillRectangle(end - 4 * i, end - 4 * j, 2, 2); } } } else { // draw diagonal lines e.gc.setForeground(resizer.getDisplay().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW)); for (int i = 1; i < min; i += 4) { e.gc.drawLine(i, y, x, i); } e.gc.setForeground(resizer.getDisplay().getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW)); for (int i = 2; i < min; i += 4) { e.gc.drawLine(i, y, x, i); } } } }); resizer.setCursor(new Cursor(resizer.getDisplay(), SWT.CURSOR_SIZESE)); MouseAdapter resizeSupport = new MouseAdapter() { private MouseMoveListener fResizeListener; @Override public void mouseDown(MouseEvent e) { Point shellSize = JFaceAbstractInformationControl.this.fShell.getSize(); final int shellX = shellSize.x; final int shellY = shellSize.y; Point mouseLoc = resizer.toDisplay(e.x, e.y); final int mouseX = mouseLoc.x; final int mouseY = mouseLoc.y; this.fResizeListener = new MouseMoveListener() { public void mouseMove(MouseEvent e2) { Point mouseLoc2 = resizer.toDisplay(e2.x, e2.y); int dx = mouseLoc2.x - mouseX; int dy = mouseLoc2.y - mouseY; setSize(shellX + dx, shellY + dy); } }; resizer.addMouseMoveListener(this.fResizeListener); } @Override public void mouseUp(MouseEvent e) { resizer.removeMouseMoveListener(this.fResizeListener); this.fResizeListener = null; } }; resizer.addMouseListener(resizeSupport); } private int getResizeHandleSize(Composite parent) { if (this.fResizeHandleSize == -1) { Slider sliderV = new Slider(parent, SWT.VERTICAL); Slider sliderH = new Slider(parent, SWT.HORIZONTAL); int width = sliderV.computeSize(SWT.DEFAULT, SWT.DEFAULT).x; int height = sliderH.computeSize(SWT.DEFAULT, SWT.DEFAULT).y; sliderV.dispose(); sliderH.dispose(); this.fResizeHandleSize = Math.min(width, height); } return this.fResizeHandleSize; } /** * Adds support to move the shell by dragging the given control. * * @param control the control that can be used to move the shell */ private void addMoveSupport(final Control control) { MouseAdapter moveSupport = new MouseAdapter() { private MouseMoveListener fMoveListener; @Override public void mouseDown(MouseEvent e) { Point shellLoc = JFaceAbstractInformationControl.this.fShell.getLocation(); final int shellX = shellLoc.x; final int shellY = shellLoc.y; Point mouseLoc = control.toDisplay(e.x, e.y); final int mouseX = mouseLoc.x; final int mouseY = mouseLoc.y; this.fMoveListener = new MouseMoveListener() { public void mouseMove(MouseEvent e2) { Point mouseLoc2 = control.toDisplay(e2.x, e2.y); int dx = mouseLoc2.x - mouseX; int dy = mouseLoc2.y - mouseY; JFaceAbstractInformationControl.this.fShell.setLocation(shellX + dx, shellY + dy); } }; control.addMouseMoveListener(this.fMoveListener); } @Override public void mouseUp(MouseEvent e) { control.removeMouseMoveListener(this.fMoveListener); this.fMoveListener = null; } }; control.addMouseListener(moveSupport); } /** * Utility to set the foreground and the background color of the given control * * @param control the control to modify * @param foreground the color to use for the foreground * @param background the color to use for the background */ private static void setColor(Control control, Color foreground, Color background) { control.setForeground(foreground); control.setBackground(background); } /** * The shell of the popup window. * * @return the shell used for the popup window */ protected final Shell getShell() { return this.fShell; } /** * The toolbar manager used to manage the toolbar, or <code>null</code> if no toolbar is shown. * * @return the tool bar manager or <code>null</code> */ protected final ToolBarManager getToolBarManager() { return this.fToolBarManager; } /** * Creates the content of this information control. Subclasses must call this method at the end of their * constructor(s). */ protected final void create() { createContent(this.fContentComposite); } /** * Creates the content of the popup window. * <p> * Implementors will usually take over {@link Composite#getBackground()} and {@link Composite#getForeground()} from * <code>parent</code>. * </p> * <p> * Implementors are expected to consider {@link #isResizable()}: If <code>true</code>, they should show scrollbars * if their content may exceed the size of the information control. If <code>false</code>, they should never show * scrollbars. * </p> * <p> * The given <code>parent</code> comes with a {@link FillLayout}. Subclasses may set a different layout. * </p> * * @param parent the container of the content */ protected abstract void createContent(Composite parent); /** * Sets the information to be presented by this information control. * <p> * The default implementation does nothing. Subclasses must either override this method or implement * {@link IInformationControlExtension2}. * * @param information the information to be presented * @see org.eclipse.jface.text.IInformationControl#setInformation(java.lang.String) */ public void setInformation(String information) { } /** * Returns whether the information control is resizable. * * @return <code>true</code> if the information control is resizable, <code>false</code> if it is not resizable. */ public boolean isResizable() { return this.fResizable; } /* * @see IInformationControl#setVisible(boolean) */ public void setVisible(boolean visible) { if (this.fShell.isVisible() == visible) { return; } this.fShell.setVisible(visible); } /* * @see IInformationControl#dispose() */ public void dispose() { if (this.fShell != null && !this.fShell.isDisposed()) { this.fShell.dispose(); } } /* * @see IInformationControl#setSize(int, int) */ public void setSize(int width, int height) { this.fShell.setSize(width, height); } /* * @see IInformationControl#setLocation(Point) */ public void setLocation(Point location) { this.fShell.setLocation(location); } /* * @see IInformationControl#setSizeConstraints(int, int) */ public void setSizeConstraints(int maxWidth, int maxHeight) { this.fSizeConstraints = new Point(maxWidth, maxHeight); } /** * Returns the size constraints. * * @return the size constraints or <code>null</code> if not set * @see #setSizeConstraints(int, int) */ protected final Point getSizeConstraints() { return this.fSizeConstraints != null ? Geometry.copy(this.fSizeConstraints) : null; } /* * @see IInformationControl#computeSizeHint() */ public Point computeSizeHint() { // XXX: Verify whether this is a good default implementation. If yes, // document it. Point constrains = getSizeConstraints(); if (constrains == null) { return this.fShell.computeSize(SWT.DEFAULT, SWT.DEFAULT, true); } return this.fShell.computeSize(constrains.x, constrains.y, true); } /** * Computes the trim (status text and tool bar are considered as trim). Subclasses can extend this method to add * additional trim (e.g. scroll bars for resizable information controls). * * @see org.eclipse.jface.text.IInformationControlExtension3#computeTrim() */ public Rectangle computeTrim() { Rectangle trim = this.fShell.computeTrim(0, 0, 0, 0); if (this.fStatusComposite != null) { trim.height += this.fStatusComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT).y; } return trim; } /* * @see org.eclipse.jface.text.IInformationControlExtension3#getBounds() */ public Rectangle getBounds() { return this.fShell.getBounds(); } /** * {@inheritDoc} * <p> * The default implementation always returns <code>false</code>. * </p> * * @see org.eclipse.jface.text.IInformationControlExtension3#restoresLocation() */ public boolean restoresLocation() { return false; } /** * {@inheritDoc} * <p> * The default implementation always returns <code>false</code>. * </p> * * @see org.eclipse.jface.text.IInformationControlExtension3#restoresSize() */ public boolean restoresSize() { return false; } /* * @see IInformationControl#addDisposeListener(DisposeListener) */ public void addDisposeListener(DisposeListener listener) { this.fShell.addDisposeListener(listener); } /* * @see IInformationControl#removeDisposeListener(DisposeListener) */ public void removeDisposeListener(DisposeListener listener) { this.fShell.removeDisposeListener(listener); } /* * @see IInformationControl#setForegroundColor(Color) */ public void setForegroundColor(Color foreground) { this.fContentComposite.setForeground(foreground); } /* * @see IInformationControl#setBackgroundColor(Color) */ public void setBackgroundColor(Color background) { this.fContentComposite.setBackground(background); } /** * {@inheritDoc} This method is not intended to be overridden by subclasses. */ public boolean isFocusControl() { return this.fShell.getDisplay().getActiveShell() == this.fShell; } /** * This default implementation sets the focus on the popup shell. Subclasses can override or extend. * * @see IInformationControl#setFocus() */ public void setFocus() { boolean focusTaken = this.fShell.setFocus(); if (!focusTaken) { this.fShell.forceFocus(); } } /** * {@inheritDoc} This method is not intended to be overridden by subclasses. */ public void addFocusListener(final FocusListener listener) { if (this.fFocusListeners.isEmpty()) { this.fShellListener = new Listener() { public void handleEvent(Event event) { Object[] listeners = JFaceAbstractInformationControl.this.fFocusListeners.getListeners(); for (Object element : listeners) { FocusListener focusListener = (FocusListener) element; if (event.type == SWT.Activate) { focusListener.focusGained(new FocusEvent(event)); } else { focusListener.focusLost(new FocusEvent(event)); } } } }; this.fShell.addListener(SWT.Deactivate, this.fShellListener); this.fShell.addListener(SWT.Activate, this.fShellListener); } this.fFocusListeners.add(listener); } /** * {@inheritDoc} This method is not intended to be overridden by subclasses. */ public void removeFocusListener(FocusListener listener) { this.fFocusListeners.remove(listener); if (this.fFocusListeners.isEmpty()) { this.fShell.removeListener(SWT.Activate, this.fShellListener); this.fShell.removeListener(SWT.Deactivate, this.fShellListener); this.fShellListener = null; } } /** * Sets the text of the status field. * <p> * The default implementation currently only updates the status field when the popup shell is not visible. The * status field can currently only be shown if the information control has been created with a non-null status field * text. * </p> * * @param statusFieldText the text to be used in the optional status field or <code>null</code> if the status field * should be hidden * @see org.eclipse.jface.text.IInformationControlExtension4#setStatusText(java.lang.String) */ public void setStatusText(String statusFieldText) { if (this.fStatusLabel != null && !getShell().isVisible()) { if (statusFieldText == null) { this.fStatusComposite.setVisible(false); } else { this.fStatusLabel.setText(statusFieldText); this.fStatusComposite.setVisible(true); } } } /* * @see org.eclipse.jface.text.IInformationControlExtension5#containsControl( org.eclipse.swt.widgets.Control) */ public boolean containsControl(Control control) { do { if (control == this.fShell) { return true; } if (control instanceof Shell) { return false; } control = control.getParent(); } while (control != null); return false; } /* * @see org.eclipse.jface.text.IInformationControlExtension5#isVisible() */ public boolean isVisible() { return this.fShell != null && !this.fShell.isDisposed() && this.fShell.isVisible(); } /** * {@inheritDoc} This default implementation returns <code>null</code>. Subclasses may override. */ public IInformationControlCreator getInformationPresenterControlCreator() { return null; } /** * Computes the size constraints based on the {@link JFaceResources#getDialogFont() dialog font}. Subclasses can * override or extend. * * @see org.eclipse.jface.text.IInformationControlExtension5#computeSizeConstraints(int, int) */ public Point computeSizeConstraints(int widthInChars, int heightInChars) { GC gc = new GC(this.fContentComposite); gc.setFont(JFaceResources.getDialogFont()); int width = gc.getFontMetrics().getAverageCharWidth(); int height = gc.getFontMetrics().getHeight(); gc.dispose(); return new Point(widthInChars * width, heightInChars * height); } }