blob: bd793f4a725e10dba06d20be0be7b216c7965437 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2010, 2019 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.internal.r.ui.rhelp;
import static org.eclipse.debug.ui.IDebugUIConstants.PREF_DETAIL_PANE_FONT;
import static org.eclipse.statet.ltk.ui.sourceediting.assist.InfoHover.MODE_FOCUS;
import java.net.URI;
import java.net.URISyntaxException;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.IHandler2;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.internal.text.html.BrowserInformationControl;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.AbstractInformationControl;
import org.eclipse.jface.text.AbstractReusableInformationControlCreator;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IInformationControlExtension2;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.LocationEvent;
import org.eclipse.swt.browser.LocationListener;
import org.eclipse.swt.browser.OpenWindowListener;
import org.eclipse.swt.browser.ProgressEvent;
import org.eclipse.swt.browser.ProgressListener;
import org.eclipse.swt.browser.TitleEvent;
import org.eclipse.swt.browser.TitleListener;
import org.eclipse.swt.browser.WindowEvent;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
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.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IWorkbenchCommandConstants;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.menus.CommandContributionItemParameter;
import org.eclipse.ui.services.IServiceLocator;
import org.eclipse.statet.ecommons.ui.SharedUIResources;
import org.eclipse.statet.ecommons.ui.actions.HandlerCollection;
import org.eclipse.statet.ecommons.ui.actions.HandlerContributionItem;
import org.eclipse.statet.ecommons.ui.actions.SimpleContributionItem;
import org.eclipse.statet.ecommons.ui.mpbv.BrowserHandler.IBrowserProvider;
import org.eclipse.statet.ecommons.ui.mpbv.BrowserHandler.NavigateBackHandler;
import org.eclipse.statet.ecommons.ui.mpbv.BrowserHandler.NavigateForwardHandler;
import org.eclipse.statet.ecommons.ui.util.InformationDispatchHandler;
import org.eclipse.statet.ecommons.ui.util.LayoutUtils;
import org.eclipse.statet.ecommons.ui.util.PixelConverter;
import org.eclipse.statet.ecommons.ui.util.UIAccess;
import org.eclipse.statet.r.core.RCore;
import org.eclipse.statet.r.ui.RUI;
import org.eclipse.statet.rhelp.core.http.RHelpHttpService;
public class RHelpInfoHoverCreator extends AbstractReusableInformationControlCreator {
public static final boolean isAvailable(final Composite parent) {
return BrowserInformationControl.isAvailable(parent);
}
public static class Data {
private final Control control;
final Object helpObject;
final URI httpUrl;
public Data(final Control control, final Object helpObject, final URI httpUrl) {
this.control= control;
this.helpObject= helpObject;
this.httpUrl= httpUrl;
}
public Control getControl() {
return this.control;
}
}
private final int mode;
public RHelpInfoHoverCreator(final int mode) {
this.mode= mode;
}
@Override
protected IInformationControl doCreateInformationControl(final Shell parent) {
return ((this.mode & MODE_FOCUS) != 0) ?
new RHelpInfoControl(parent, this.mode, true) :
new RHelpInfoControl(parent, this.mode);
}
}
class RHelpInfoControl extends AbstractInformationControl implements IInformationControlExtension2,
IPropertyChangeListener, OpenWindowListener, LocationListener, ProgressListener, TitleListener,
IBrowserProvider {
/** Action id (command) to navigate one page back. */
protected static final String NAVIGATE_BACK_COMMAND_ID= IWorkbenchCommandConstants.NAVIGATE_BACK;
/** Action id (command) to navigate one page forward. */
protected static final String NAVIGATE_FORWARD_COMMAND_ID= IWorkbenchCommandConstants.NAVIGATE_FORWARD;
/**
* Cached scroll bar width and height
*/
private static Point gScrollBarSize;
private final int mode;
private RHelpLabelProvider labelProvider;
private Composite contentComposite;
private Label titleImage;
private StyledText titleText;
private Browser infoBrowser;
private final HandlerCollection handlerCollection= new HandlerCollection();
private boolean layoutWorkaround;
private boolean layoutHint;
private RHelpInfoHoverCreator.Data input;
private boolean inputChanged;
private boolean loadingCompleted;
private String browserTitle;
private boolean hide;
RHelpInfoControl(final Shell shell, final int mode) {
super(shell, ""); //$NON-NLS-1$
assert ((mode & MODE_FOCUS) == 0);
this.mode= mode;
JFaceResources.getFontRegistry().addListener(this);
create();
}
RHelpInfoControl(final Shell shell, final int mode, final boolean dummy) {
super(shell, new ToolBarManager(SWT.FLAT));
assert ((mode & MODE_FOCUS) != 0);
this.mode= mode;
create();
}
@Override
public void setInput(final Object input) {
this.inputChanged= true;
if (input instanceof RHelpInfoHoverCreator.Data) {
this.input= (RHelpInfoHoverCreator.Data) input;
}
else {
this.input= null;
}
}
@Override
public boolean hasContents() {
return (this.input != null);
}
@Override
protected void createContent(final Composite parent) {
this.contentComposite= new Composite(parent, SWT.NONE) {
@Override
public Point computeSize(final int width, final int height, final boolean changed) {
return super.computeSize(width, height, changed || width != getSize().x);
}
};
this.contentComposite.setBackgroundMode(SWT.INHERIT_FORCE);
final GridLayout gridLayout= LayoutUtils.newCompositeGrid(2);
gridLayout.horizontalSpacing= (int) ((gridLayout.horizontalSpacing) / 1.5);
this.contentComposite.setLayout(gridLayout);
final int vIndent= Math.max(1, LayoutUtils.defaultVSpacing() / 4);
final int hIndent= Math.max(3, LayoutUtils.defaultHSpacing() / 3);
{ // Title image
this.titleImage= new Label(this.contentComposite, SWT.NULL);
final Image image= SharedUIResources.getImages().get(SharedUIResources.PLACEHOLDER_IMAGE_ID);
this.titleImage.setImage(image);
final GridData textGd= new GridData(SWT.FILL, SWT.TOP, false, false);
this.titleText= new StyledText(this.contentComposite, SWT.MULTI | SWT.READ_ONLY | SWT.WRAP) {
@Override
public Point computeSize(int width, final int height, final boolean changed) {
if (!RHelpInfoControl.this.layoutHint && width <= 0 && RHelpInfoControl.this.contentComposite.getSize().x > 0) {
width= RHelpInfoControl.this.contentComposite.getSize().x -
LayoutUtils.defaultHMargin() - RHelpInfoControl.this.titleImage.getSize().x - LayoutUtils.defaultHSpacing() - 10;
}
final Point size= super.computeSize(width, -1, true);
// if (width >= 0) {
// size.x= Math.min(size.x, width);
// }
return size;
}
};
this.titleText.setFont(JFaceResources.getDialogFont());
final GC gc= new GC(this.titleText);
final FontMetrics fontMetrics= gc.getFontMetrics();
final GridData imageGd= new GridData(SWT.FILL, SWT.TOP, false, false);
imageGd.horizontalIndent= hIndent - 2;
final int textHeight= fontMetrics.getAscent() + fontMetrics.getLeading();
final int imageHeight= image.getBounds().height;
final int shift= Math.max(3, (int) ((fontMetrics.getDescent()) / 1.5));
if (textHeight+shift < imageHeight) {
imageGd.verticalIndent= vIndent+shift;
textGd.verticalIndent= vIndent+(imageHeight-textHeight);
}
else {
imageGd.verticalIndent= vIndent+(textHeight-imageHeight)+shift;
textGd.verticalIndent= vIndent;
}
this.titleImage.setLayoutData(imageGd);
this.titleText.setLayoutData(textGd);
this.layoutWorkaround= true;
gc.dispose();
}
this.infoBrowser= new Browser(this.contentComposite, SWT.NONE);
this.infoBrowser.addOpenWindowListener(this);
this.infoBrowser.addLocationListener(this);
this.infoBrowser.addProgressListener(this);
this.infoBrowser.addTitleListener(this);
this.infoBrowser.addOpenWindowListener(new OpenWindowListener() {
@Override
public void open(final WindowEvent event) {
event.required= true;
}
});
// Disable context menu
this.infoBrowser.setMenu(new Menu(getShell(), SWT.NONE));
final GridData gd= new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1);
// gd.widthHint= LayoutUtils.hintWidth(fInfoText, INFO_FONT, 50);
this.infoBrowser.setLayoutData(gd);
this.infoBrowser.addKeyListener(new KeyListener() {
@Override
public void keyPressed(final KeyEvent e) {
if (e.character == SWT.ESC) {
dispose();
}
}
@Override
public void keyReleased(final KeyEvent e) {}
});
setBackgroundColor(getShell().getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND));
setForegroundColor(getShell().getDisplay().getSystemColor(SWT.COLOR_INFO_FOREGROUND));
if ((this.mode & MODE_FOCUS) != 0) {
initActions(this.handlerCollection);
final ToolBarManager toolBarManager= getToolBarManager();
contributeToActionBars(PlatformUI.getWorkbench(), toolBarManager, this.handlerCollection);
toolBarManager.update(true);
}
updateInput();
}
protected void initActions(final HandlerCollection handlers) {
{ final IHandler2 handler= new NavigateBackHandler(this);
handlers.add(NAVIGATE_BACK_COMMAND_ID, handler);
// handlerService.activateHandler(NAVIGATE_BACK_COMMAND_ID, handler);
// handlerService.activateHandler(IWorkbenchCommandConstants.NAVIGATE_BACKWARD_HISTORY, handler);
}
{ final IHandler2 handler= new NavigateForwardHandler(this);
handlers.add(NAVIGATE_FORWARD_COMMAND_ID, handler);
// handlerService.activateHandler(NAVIGATE_FORWARD_COMMAND_ID, handler);
// handlerService.activateHandler(IWorkbenchCommandConstants.NAVIGATE_FORWARD_HISTORY, handler);
}
}
protected void contributeToActionBars(final IServiceLocator serviceLocator,
final ToolBarManager toolBarManager, final HandlerCollection handlers) {
toolBarManager.add(
new HandlerContributionItem(new CommandContributionItemParameter(
serviceLocator, null, NAVIGATE_BACK_COMMAND_ID, HandlerContributionItem.STYLE_PUSH),
handlers.get(NAVIGATE_BACK_COMMAND_ID) ));
toolBarManager.add(
new HandlerContributionItem(new CommandContributionItemParameter(
serviceLocator, null, NAVIGATE_FORWARD_COMMAND_ID, HandlerContributionItem.STYLE_PUSH),
handlers.get(NAVIGATE_FORWARD_COMMAND_ID) ));
toolBarManager.add(new Separator());
toolBarManager.add(new SimpleContributionItem(
"Show in R Help View", "V",
RUI.getImageDescriptor(RUI.IMG_OBJ_R_HELP_SEARCH), null ) {
@Override
protected void execute() throws ExecutionException {
if (UIAccess.isOkToUse(RHelpInfoControl.this.infoBrowser)) {
try {
final String urlString= RHelpInfoControl.this.infoBrowser.getUrl();
final IWorkbenchPage page= UIAccess.getActiveWorkbenchPage(true);
final RHelpView view= (RHelpView) page.showView(RUI.R_HELP_VIEW_ID);
final URI browseUrl= RCore.getRHelpHttpService().toHttpUrl(urlString,
null, RHelpHttpService.BROWSE_TARGET );
if (browseUrl != null) {
view.openUrl(browseUrl, null);
}
else {
view.openUrl(urlString, null);
}
}
catch (final Exception e) {}
}
}
});
}
@Override
public Browser getBrowser() {
return this.infoBrowser;
}
@Override
public void showMessage(final IStatus status) {
}
@Override
public void changing(final LocationEvent event) {
if (event.location.startsWith("http://")) { //$NON-NLS-1$
try {
if (RCore.getRHelpHttpService().isDynamicUrl(new URI(event.location))) {
return;
}
}
catch (final Exception e) {}
}
if (event.location.equals("about:blank")) { //$NON-NLS-1$
return;
}
event.doit= false;
}
@Override
public void changed(final LocationEvent event) {
if (!event.top) {
return;
}
final String url= this.infoBrowser.getUrl();
final Object obj= RCore.getRHelpHttpService().getContentOfUrl(url);
updateTitle(obj, this.browserTitle);
}
@Override
public void changed(final ProgressEvent event) {
this.handlerCollection.update(null);
}
@Override
public void completed(final ProgressEvent event) {
this.loadingCompleted= true;
this.handlerCollection.update(null);
}
@Override
public void changed(final TitleEvent event) {
String title= event.title;
if (title == null) {
title= ""; //$NON-NLS-1$
}
else if (title.startsWith("http://")) { //$NON-NLS-1$
final int idx= title.lastIndexOf('/');
if (idx >= 0) {
title= title.substring(idx+1);
}
}
this.browserTitle= title;
}
@Override
public void open(final WindowEvent event) {
event.required= true;
}
@Override
public void setBackgroundColor(final Color background) {
super.setBackgroundColor(background);
this.contentComposite.setBackground(background);
this.infoBrowser.setBackground(background);
}
@Override
public void setForegroundColor(final Color foreground) {
super.setForegroundColor(foreground);
this.contentComposite.setForeground(foreground);
this.titleText.setForeground(foreground);
this.infoBrowser.setForeground(foreground);
}
@Override
public void setSize(final int width, final int height) {
this.infoBrowser.setRedraw(false); // avoid flickering
try {
super.setSize(width, height);
}
finally {
this.infoBrowser.setRedraw(true);
}
}
@Override
public Rectangle computeTrim() {
final Rectangle trim= super.computeTrim();
final Rectangle textTrim= this.infoBrowser.computeTrim(0, 0, 0, 0);
if ((this.mode & MODE_FOCUS) != 0 && textTrim.width == 0) {
if (gScrollBarSize == null) {
final Text text= new Text(this.contentComposite, SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL | SWT.BORDER);
gScrollBarSize= new Point(
text.getVerticalBar().getSize().x,
text.getHorizontalBar().getSize().y);
text.dispose();
}
textTrim.x= 0;
textTrim.y= 0;
textTrim.width= gScrollBarSize.x;
textTrim.height= gScrollBarSize.y;
}
trim.x+= textTrim.x;
trim.y+= textTrim.y;
trim.width+= textTrim.width;
trim.height+= textTrim.height;
return trim;
}
@Override
public Point computeSizeHint() {
updateInput();
final Point sizeConstraints= getSizeConstraints();
final Rectangle trim= computeTrim();
this.titleText.setFont(JFaceResources.getDialogFont());
final PixelConverter converter= new PixelConverter(this.titleText);
int widthHint= converter.convertWidthInCharsToPixels(60);
final GC gc= new GC(this.contentComposite);
gc.setFont(JFaceResources.getTextFont());
widthHint= Math.max(widthHint, gc.getFontMetrics().getAverageCharWidth() * 60);
gc.dispose();
final int heightHint= this.titleText.getLineHeight() * 12;
final int widthMax= ((sizeConstraints != null && sizeConstraints.x != SWT.DEFAULT) ?
sizeConstraints.x : widthHint+100) - trim.width;
final int heightMax= ((sizeConstraints != null && sizeConstraints.y != SWT.DEFAULT) ?
sizeConstraints.y : this.titleText.getLineHeight()*12) - trim.height;
final Point size= new Point(widthHint, heightHint);
size.y+= LayoutUtils.defaultVSpacing();
size.x= Math.max(Math.min(size.x, widthMax), 200) + trim.width;
size.y= Math.max(Math.min(size.y, heightMax), 100) + trim.height;
return size;
}
@Override
public Point computeSizeConstraints(final int widthInChars, final int heightInChars) {
final int width= LayoutUtils.hintWidth(this.titleText, JFaceResources.DIALOG_FONT, widthInChars);
final int lineHeight= this.titleText.getLineHeight();
return new Point(width, lineHeight * heightInChars + LayoutUtils.defaultVSpacing());
}
@Override
public void setVisible(final boolean visible) {
if (visible) {
this.hide= false;
updateInput();
waitCompleted();
if (this.hide || !UIAccess.isOkToUse(getShell())) {
return;
}
if (this.layoutWorkaround) {
this.contentComposite.layout(true, true);
this.layoutWorkaround= false;
}
if (Platform.WS_WIN32.equals(SWT.getPlatform())) {
getShell().moveAbove(null);
}
}
else {
this.hide= true;
}
super.setVisible(visible);
}
private void waitCompleted() {
final Display display= Display.getCurrent();
display.timerExec(200, new Runnable() {
@Override
public void run() {
RHelpInfoControl.this.loadingCompleted= true;
}
});
while (!this.loadingCompleted) {
// Drive the event loop to process the events required to load the browser widget's contents:
if (!display.readAndDispatch()) {
display.sleep();
}
}
}
@Override
public void setFocus() {
this.infoBrowser.setFocus();
}
private void updateInput() {
if (this.infoBrowser == null || !this.inputChanged) {
return;
}
if (this.labelProvider == null) {
this.labelProvider= new RHelpLabelProvider(RHelpLabelProvider.WITH_TITLE | RHelpLabelProvider.WITH_QUALIFIER | RHelpLabelProvider.HEADER);
}
this.loadingCompleted= false;
this.inputChanged= false;
this.browserTitle= null;
updateTitle(this.input.helpObject, null);
if (this.input != null && this.input.httpUrl != null) {
URI url= this.input.httpUrl;
if ((this.mode & MODE_FOCUS) == 0) { // disable scrollbars
try {
url= new URI(url.toString() +
'?' + RHelpUIServlet.STYLE_PARAM + '=' + RHelpUIServlet.HOVER_STYLE );
}
catch (final URISyntaxException e) {}
}
this.infoBrowser.setUrl(url.toString());
}
else {
this.infoBrowser.setUrl("about:blank"); //$NON-NLS-1$
}
if ((this.mode & MODE_FOCUS) == 0) {
setStatusText((this.input.getControl() != null
&& this.input.getControl().isFocusControl() ) ?
InformationDispatchHandler.getAffordanceString(this.mode) : "" ); //$NON-NLS-1$
}
}
private void updateTitle(final Object helpObject, final String alt) {
if (helpObject != null) {
final Image image= this.labelProvider.getImage(helpObject);
this.titleImage.setImage((image != null) ? image : SharedUIResources.getImages().get(SharedUIResources.PLACEHOLDER_IMAGE_ID));
final StyledString styleString= this.labelProvider.getStyledText(helpObject);
this.titleText.setText(styleString.getString());
this.titleText.setStyleRanges(styleString.getStyleRanges());
}
else {
this.titleImage.setImage(SharedUIResources.getImages().get(SharedUIResources.PLACEHOLDER_IMAGE_ID));
this.titleText.setText((alt != null) ? alt : ""); //$NON-NLS-1$
}
}
@Override
public IInformationControlCreator getInformationPresenterControlCreator() {
// enriched mode
return new RHelpInfoHoverCreator(this.mode | MODE_FOCUS);
}
@Override
public void propertyChange(final PropertyChangeEvent event) {
final String property= event.getProperty();
if (property.equals(PREF_DETAIL_PANE_FONT) || property.equals(JFaceResources.DEFAULT_FONT)) {
dispose();
}
}
@Override
public void dispose() {
JFaceResources.getFontRegistry().removeListener(this);
this.handlerCollection.dispose();
super.dispose();
}
}