blob: af8b9852fd00a9b38df56e1ef47d056c098c5d8c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2006 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
*******************************************************************************/
package org.eclipse.jdt.internal.ui.text.java.hover;
import java.io.IOException;
import java.io.StringReader;
import java.util.Iterator;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTError;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.LocationAdapter;
import org.eclipse.swt.browser.LocationEvent;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
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.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.TextLayout;
import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
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.Menu;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlExtension;
import org.eclipse.jface.text.IInformationControlExtension3;
import org.eclipse.jface.text.TextPresentation;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.text.HTML2TextReader;
import org.eclipse.jdt.internal.ui.text.HTMLPrinter;
import org.eclipse.jdt.internal.ui.text.IInformationControlExtension4;
/**
* Displays textual information in a {@link org.eclipse.swt.browser.Browser}
* widget.
*
* <p>
* This class may be instantiated; it is not intended to be subclassed.
* </p>
* <p>
* Current problems:
* - the size computation is too small
* - focusLost event is not sent (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=84532)
* </p>
*
* @since 3.1
*/
public class BrowserInformationControl implements IInformationControl, IInformationControlExtension, IInformationControlExtension3, IInformationControlExtension4, DisposeListener {
/**
* Tells whether the SWT Browser widget and hence this information
* control is available.
*
* @param parent the parent component used for checking or <code>null</code> if none
* @return <code>true</code> if this control is available
*/
public static boolean isAvailable(Composite parent) {
if (!fgAvailabilityChecked) {
try {
if (parent == null)
parent= JavaPlugin.getActiveWorkbenchShell();
if (parent == null)
return false; // don't store this value - try again later
Browser browser= new Browser(parent, SWT.NONE);
browser.dispose();
fgIsAvailable= true;
} catch (SWTError er) {
fgIsAvailable= false;
} finally {
fgAvailabilityChecked= true;
}
}
return fgIsAvailable;
}
/** Border thickness in pixels. */
private static final int BORDER= 1;
/**
* Minimal size constraints.
* @since 3.2
*/
private static final int MIN_WIDTH= 80;
private static final int MIN_HEIGHT= 80;
/**
* Availability checking cache.
*/
private static boolean fgIsAvailable= false;
private static boolean fgAvailabilityChecked= false;
/** The control's shell */
private Shell fShell;
/** The control's browser widget */
private Browser fBrowser;
/** Tells whether the browser has content */
private boolean fBrowserHasContent;
/** The control width constraint */
private int fMaxWidth= -1;
/** The control height constraint */
private int fMaxHeight= -1;
private Font fStatusTextFont;
private Label fStatusTextField;
private String fStatusFieldText;
private boolean fHideScrollBars;
private Listener fDeactivateListener;
private ListenerList fFocusListeners= new ListenerList();
private Label fSeparator;
private String fInputText;
private TextLayout fTextLayout;
private TextStyle fBoldStyle;
/**
* Creates a default information control with the given shell as parent. The given
* information presenter is used to process the information to be displayed. The given
* styles are applied to the created styled text widget.
*
* @param parent the parent shell
* @param shellStyle the additional styles for the shell
* @param style the additional styles for the styled text widget
*/
public BrowserInformationControl(Shell parent, int shellStyle, int style) {
this(parent, shellStyle, style, null);
}
/**
* Creates a default information control with the given shell as parent. The given
* information presenter is used to process the information to be displayed. The given
* styles are applied to the created styled text widget.
*
* @param parent the parent shell
* @param shellStyle the additional styles for the shell
* @param style the additional styles for the styled text widget
* @param statusFieldText the text to be used in the optional status field
* or <code>null</code> if the status field should be hidden
*/
public BrowserInformationControl(Shell parent, int shellStyle, int style, String statusFieldText) {
fStatusFieldText= statusFieldText;
fShell= new Shell(parent, SWT.NO_FOCUS | SWT.ON_TOP | shellStyle);
Display display= fShell.getDisplay();
fShell.setBackground(display.getSystemColor(SWT.COLOR_BLACK));
fTextLayout= new TextLayout(display);
Composite composite= fShell;
GridLayout layout= new GridLayout(1, false);
int border= ((shellStyle & SWT.NO_TRIM) == 0) ? 0 : BORDER;
layout.marginHeight= border;
layout.marginWidth= border;
composite.setLayout(layout);
if (statusFieldText != null) {
composite= new Composite(composite, SWT.NONE);
layout= new GridLayout(1, false);
layout.marginHeight= 0;
layout.marginWidth= 0;
layout.verticalSpacing= 1;
layout.horizontalSpacing= 1;
composite.setLayout(layout);
GridData gd= new GridData(GridData.FILL_BOTH);
composite.setLayoutData(gd);
composite.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND));
composite.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
}
// Browser field
fBrowser= new Browser(composite, SWT.NONE);
fHideScrollBars= (style & SWT.V_SCROLL) == 0 && (style & SWT.H_SCROLL) == 0;
GridData gd= new GridData(GridData.BEGINNING | GridData.FILL_BOTH);
fBrowser.setLayoutData(gd);
fBrowser.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND));
fBrowser.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
fBrowser.addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent e) {
if (e.character == 0x1B) // ESC
fShell.dispose();
}
public void keyReleased(KeyEvent e) {}
});
/*
* XXX revisit when the Browser support is better
* See https://bugs.eclipse.org/bugs/show_bug.cgi?id=107629. Choosing a link to a
* non-available target will show an error dialog behind the ON_TOP shell that seemingly
* blocks the workbench. Disable links completely for now.
*/
fBrowser.addLocationListener(new LocationAdapter() {
/*
* @see org.eclipse.swt.browser.LocationAdapter#changing(org.eclipse.swt.browser.LocationEvent)
*/
public void changing(LocationEvent event) {
String location= event.location;
/*
* Using the Browser.setText API triggers a location change to "about:blank" with
* the mozilla widget. The Browser on carbon uses yet another kind of special
* initialization URLs.
* TODO remove this code once https://bugs.eclipse.org/bugs/show_bug.cgi?id=130314 is fixed
*/
if (!"about:blank".equals(location) && !("carbon".equals(SWT.getPlatform()) && location.startsWith("applewebdata:"))) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
event.doit= false;
}
});
// Replace browser's built-in context menu with none
fBrowser.setMenu(new Menu(fShell, SWT.NONE));
// Status field
if (statusFieldText != null) {
fSeparator= new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL | SWT.LINE_DOT);
fSeparator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
// Status field label
fStatusTextField= new Label(composite, SWT.RIGHT);
fStatusTextField.setText(statusFieldText);
Font font= fStatusTextField.getFont();
FontData[] fontDatas= font.getFontData();
for (int i= 0; i < fontDatas.length; i++)
fontDatas[i].setHeight(fontDatas[i].getHeight() * 9 / 10);
fStatusTextFont= new Font(fStatusTextField.getDisplay(), fontDatas);
fStatusTextField.setFont(fStatusTextFont);
gd= new GridData(GridData.FILL_HORIZONTAL | GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_BEGINNING);
fStatusTextField.setLayoutData(gd);
fStatusTextField.setForeground(display.getSystemColor(SWT.COLOR_WIDGET_DARK_SHADOW));
fStatusTextField.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
}
addDisposeListener(this);
createTextLayout();
}
/**
* Creates a default information control with the given shell as parent. The given
* information presenter is used to process the information to be displayed. The given
* styles are applied to the created styled text widget.
*
* @param parent the parent shell
* @param style the additional styles for the browser widget
*/
public BrowserInformationControl(Shell parent,int style) {
this(parent, SWT.TOOL | SWT.NO_TRIM, style);
}
/**
* Creates a default information control with the given shell as parent.
* No information presenter is used to process the information
* to be displayed. No additional styles are applied to the styled text widget.
*
* @param parent the parent shell
*/
public BrowserInformationControl(Shell parent) {
this(parent, SWT.NONE);
}
/*
* @see IInformationControl#setInformation(String)
*/
public void setInformation(String content) {
fBrowserHasContent= content != null && content.length() > 0;
if (!fBrowserHasContent)
content= "<html><body ></html>"; //$NON-NLS-1$
fInputText= content;
int shellStyle= fShell.getStyle();
boolean RTL= (shellStyle & SWT.RIGHT_TO_LEFT) != 0;
String[] styles= null;
if (RTL && !fHideScrollBars)
styles= new String[] { "direction:rtl;", "word-wrap:break-word;" }; //$NON-NLS-1$ //$NON-NLS-2$
else if (RTL && fHideScrollBars)
styles= new String[] { "direction:rtl;", "overflow:hidden;", "word-wrap:break-word;" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
else if (fHideScrollBars && true)
styles= new String[] { "overflow:hidden;", "word-wrap: break-word;" }; //$NON-NLS-1$ //$NON-NLS-2$
if (styles != null) {
StringBuffer buffer= new StringBuffer(content);
HTMLPrinter.insertStyles(buffer, styles);
content= buffer.toString();
}
fBrowser.setText(content);
}
/*
* @see org.eclipse.jdt.internal.ui.text.IInformationControlExtension4#setStatusText(java.lang.String)
* @since 3.2
*/
public void setStatusText(String statusFieldText) {
fStatusFieldText= statusFieldText;
}
/*
* @see IInformationControl#setVisible(boolean)
*/
public void setVisible(boolean visible) {
if (fShell.isVisible() == visible)
return;
if (visible) {
if (fStatusTextField != null) {
boolean state= fStatusFieldText != null;
if (state)
fStatusTextField.setText(fStatusFieldText);
fStatusTextField.setVisible(state);
fSeparator.setVisible(state);
}
}
fShell.setVisible(visible);
if (!visible)
setInformation(""); //$NON-NLS-1$
}
/**
* Creates and initializes the text layout used
* to compute the size hint.
*
* @since 3.2
*/
private void createTextLayout() {
fTextLayout= new TextLayout(fBrowser.getDisplay());
// Initialize fonts
Font font= fBrowser.getFont();
fTextLayout.setFont(font);
fTextLayout.setWidth(-1);
FontData[] fontData= font.getFontData();
for (int i= 0; i < fontData.length; i++)
fontData[i].setStyle(SWT.BOLD);
font= new Font(fShell.getDisplay(), fontData);
fBoldStyle= new TextStyle(font, null, null);
// Compute and set tab width
fTextLayout.setText(" "); //$NON-NLS-1$
int tabWidth = fTextLayout.getBounds().width;
fTextLayout.setTabs(new int[] {tabWidth});
fTextLayout.setText(""); //$NON-NLS-1$
}
/*
* @see IInformationControl#dispose()
*/
public void dispose() {
fTextLayout.dispose();
fTextLayout= null;
fBoldStyle.font.dispose();
fBoldStyle= null;
if (fShell != null && !fShell.isDisposed())
fShell.dispose();
else
widgetDisposed(null);
}
/*
* @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent)
*/
public void widgetDisposed(DisposeEvent event) {
if (fStatusTextFont != null && !fStatusTextFont.isDisposed())
fStatusTextFont.dispose();
fShell= null;
fBrowser= null;
fStatusTextFont= null;
}
/*
* @see IInformationControl#setSize(int, int)
*/
public void setSize(int width, int height) {
fShell.setSize(Math.min(width, fMaxWidth), Math.min(height, fMaxHeight));
}
/*
* @see IInformationControl#setLocation(Point)
*/
public void setLocation(Point location) {
fShell.setLocation(location);
}
/*
* @see IInformationControl#setSizeConstraints(int, int)
*/
public void setSizeConstraints(int maxWidth, int maxHeight) {
fMaxWidth= maxWidth;
fMaxHeight= maxHeight;
}
/*
* @see IInformationControl#computeSizeHint()
*/
public Point computeSizeHint() {
TextPresentation presentation= new TextPresentation();
HTML2TextReader reader= new HTML2TextReader(new StringReader(fInputText), presentation);
String text;
try {
text= reader.getString();
} catch (IOException e) {
text= ""; //$NON-NLS-1$
}
fTextLayout.setText(text);
Iterator iter= presentation.getAllStyleRangeIterator();
while (iter.hasNext()) {
StyleRange sr= (StyleRange)iter.next();
if (sr.fontStyle == SWT.BOLD)
fTextLayout.setStyle(fBoldStyle, sr.start, sr.start + sr.length - 1);
}
Rectangle bounds= fTextLayout.getBounds();
int width= bounds.width;
int height= bounds.height;
width += 15;
height += 25;
if (fStatusFieldText != null && fSeparator != null) {
fTextLayout.setText(fStatusFieldText);
Rectangle statusBounds= fTextLayout.getBounds();
Rectangle separatorBounds= fSeparator.getBounds();
width= Math.max(width, statusBounds.width);
height= height + statusBounds.height + separatorBounds.height;
}
// Apply size constraints
if (fMaxWidth != SWT.DEFAULT)
width= Math.min(fMaxWidth, width);
if (fMaxHeight != SWT.DEFAULT)
height= Math.min(fMaxHeight, height);
// Ensure minimal size
width= Math.max(MIN_WIDTH, width);
height= Math.max(MIN_HEIGHT, height);
return new Point(width, height);
}
/*
* @see org.eclipse.jface.text.IInformationControlExtension3#computeTrim()
*/
public Rectangle computeTrim() {
return fShell.computeTrim(0, 0, 0, 0);
}
/*
* @see org.eclipse.jface.text.IInformationControlExtension3#getBounds()
*/
public Rectangle getBounds() {
return fShell.getBounds();
}
/*
* @see org.eclipse.jface.text.IInformationControlExtension3#restoresLocation()
*/
public boolean restoresLocation() {
return false;
}
/*
* @see org.eclipse.jface.text.IInformationControlExtension3#restoresSize()
*/
public boolean restoresSize() {
return false;
}
/*
* @see IInformationControl#addDisposeListener(DisposeListener)
*/
public void addDisposeListener(DisposeListener listener) {
fShell.addDisposeListener(listener);
}
/*
* @see IInformationControl#removeDisposeListener(DisposeListener)
*/
public void removeDisposeListener(DisposeListener listener) {
fShell.removeDisposeListener(listener);
}
/*
* @see IInformationControl#setForegroundColor(Color)
*/
public void setForegroundColor(Color foreground) {
fBrowser.setForeground(foreground);
}
/*
* @see IInformationControl#setBackgroundColor(Color)
*/
public void setBackgroundColor(Color background) {
fBrowser.setBackground(background);
}
/*
* @see IInformationControl#isFocusControl()
*/
public boolean isFocusControl() {
return fBrowser.isFocusControl();
}
/*
* @see IInformationControl#setFocus()
*/
public void setFocus() {
fShell.forceFocus();
fBrowser.setFocus();
}
/*
* @see IInformationControl#addFocusListener(FocusListener)
*/
public void addFocusListener(final FocusListener listener) {
fBrowser.addFocusListener(listener);
/*
* FIXME: This is a workaround for bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=84532
* (Browser widget does not send focusLost event)
*/
if (fFocusListeners.isEmpty()) {
fDeactivateListener= new Listener() {
public void handleEvent(Event event) {
Object[] listeners= fFocusListeners.getListeners();
for (int i = 0; i < listeners.length; i++)
((FocusListener)listeners[i]).focusLost(new FocusEvent(event));
}
};
fBrowser.getShell().addListener(SWT.Deactivate, fDeactivateListener);
}
fFocusListeners.add(listener);
}
/*
* @see IInformationControl#removeFocusListener(FocusListener)
*/
public void removeFocusListener(FocusListener listener) {
fBrowser.removeFocusListener(listener);
/*
* FIXME: This is a workaround for bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=84532
* (Browser widget does not send focusLost event)
*/
fFocusListeners.remove(listener);
if (fFocusListeners.isEmpty()) {
fBrowser.getShell().removeListener(SWT.Deactivate, fDeactivateListener);
fDeactivateListener= null;
}
}
/*
* @see IInformationControlExtension#hasContents()
*/
public boolean hasContents() {
return fBrowserHasContent;
}
}