blob: 99b895b7225e9d4718fa71400fffff0c191d15f2 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2010, 2021 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 java.io.PrintWriter;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.preference.PreferenceConverter;
import org.eclipse.jface.resource.FontDescriptor;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.internal.editors.text.EditorsPlugin;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.ecommons.preferences.PreferencesUtil;
import org.eclipse.statet.ecommons.preferences.SettingsChangeNotifier;
import org.eclipse.statet.ecommons.preferences.core.util.PreferenceUtils;
import org.eclipse.statet.ecommons.ui.util.LayoutUtils;
import org.eclipse.statet.ecommons.ui.util.UIAccess;
import org.eclipse.statet.internal.r.ui.RIdentifierGroups;
import org.eclipse.statet.internal.r.ui.RUIPlugin;
import org.eclipse.statet.r.core.RCore;
import org.eclipse.statet.r.ui.RUIPreferenceConstants;
import org.eclipse.statet.rhelp.core.http.RHelpHttpServlet;
import org.eclipse.statet.rhelp.core.http.jetty.JettyForwardHandler;
import org.eclipse.statet.rhelp.core.http.jetty.JettyRHelpUtils;
@NonNullByDefault
public class RHelpUIServlet extends RHelpHttpServlet implements IPropertyChangeListener, SettingsChangeNotifier.ChangeListener {
private static final long serialVersionUID= 1L;
static final String STYLE_PARAM= "style"; //$NON-NLS-1$
static final String HOVER_STYLE= "hover"; //$NON-NLS-1$
private static final String[] DEFAULT_MATCH_COLORS= new String[] {
".SMATCH-A { background: #ceccf7; }\n",
".SMATCH-B { background: #ffffcf; }\n",
".SMATCH-C { background: aquamarine; }\n",
".SMATCH-D { background: palegreen; }\n",
".SMATCH-E { background: coral; }\n",
".SMATCH-F { background: wheat; }\n",
".SMATCH-G { background: khaki; }\n",
".SMATCH-H { background: lime; }\n",
".SMATCH-I { background: deepskyblue; }\n",
".SMATCH-J { background: plum; }\n",
};
private static void appendCssColor(final StringBuilder sb, final RGB color) {
sb.append('#');
String s= Integer.toHexString(color.red);
if (s.length() == 1) {
sb.append('0');
}
sb.append(s);
s= Integer.toHexString(color.green);
if (s.length() == 1) {
sb.append('0');
}
sb.append(s);
s= Integer.toHexString(color.blue);
if (s.length() == 1) {
sb.append('0');
}
sb.append(s);
}
private static void appendELinkColors(final StringBuilder sb, final RGB foregroundColor) {
final RGB hyperlinkColor= JFaceResources.getColorRegistry().getRGB(JFacePreferences.HYPERLINK_COLOR);
sb.append("a { color: "); //$NON-NLS-1$
appendCssColor(sb, hyperlinkColor);
sb.append("; }\n");//$NON-NLS-1$
sb.append("a:hover, a:active, a:focus { color: "); //$NON-NLS-1$
appendCssColor(sb, JFaceResources.getColorRegistry().getRGB(JFacePreferences.ACTIVE_HYPERLINK_COLOR));
sb.append("; }\n"); //$NON-NLS-1$
sb.append("a:visited { color: "); //$NON-NLS-1$
appendCssColor(sb, new RGB(
(hyperlinkColor.red + ((hyperlinkColor.red <= 127) ? +64 : -64)),
(hyperlinkColor.green + foregroundColor.green) / 2,
(hyperlinkColor.blue + ((hyperlinkColor.blue > 32) ? -32 : +32) + foregroundColor.blue ) / 2 ));
sb.append("; }\n"); //$NON-NLS-1$
}
private static void appendMatchColors(final StringBuilder sb) {
int i= 0;
final RGB rgb= PreferenceConverter.getColor(
EditorsPlugin.getDefault().getPreferenceStore(),
"searchResultIndicationColor"); //$NON-NLS-1$
if (rgb != null) {
sb.append(".SMATCH-A { background: ");
appendCssColor(sb, rgb);
sb.append("; }\n"); //$NON-NLS-1$
i++;
}
while (i < DEFAULT_MATCH_COLORS.length) {
sb.append(DEFAULT_MATCH_COLORS[i++]);
}
}
public static class Browse extends RHelpUIServlet {
private static final long serialVersionUID= 1L;
@Override
protected void collectCss(final StringBuilder sb) {
final RGB foregroundColor= JFaceResources.getColorRegistry().getRGB("org.eclipse.statet.workbench.themes.DocViewColor"); //$NON-NLS-1$
final RGB docBackgroundColor= JFaceResources.getColorRegistry().getRGB("org.eclipse.statet.workbench.themes.DocViewBackgroundColor"); //$NON-NLS-1$
final RGB borderColor= Display.getCurrent().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW).getRGB();
{ final FontDescriptor docFontDescr= JFaceResources.getFontDescriptor("org.eclipse.statet.workbench.themes.DocViewFont"); //$NON-NLS-1$
final FontData fontData= docFontDescr.getFontData()[0];
sb.append("body { font-family: '"); //$NON-NLS-1$
sb.append(fontData.getName());
sb.append("'; font-size:"); //$NON-NLS-1$
sb.append(fontData.getHeight());
sb.append("pt; color:"); //$NON-NLS-1$
appendCssColor(sb, foregroundColor);
sb.append("; background:"); //$NON-NLS-1$
appendCssColor(sb, docBackgroundColor);
sb.append("; }\n"); //$NON-NLS-1$
}
appendELinkColors(sb, foregroundColor);
sb.append("div.toc a, div.toc a:visited { color: "); //$NON-NLS-1$
appendCssColor(sb, foregroundColor);
sb.append("; }\n"); //$NON-NLS-1$
sb.append("@media screen {\n"); //$NON-NLS-1$
sb.append("body { margin: "); //$NON-NLS-1$
sb.append(LayoutUtils.defaultVSpacing());
sb.append("px "); //$NON-NLS-1$
sb.append(LayoutUtils.defaultHSpacing());
sb.append("px; }\n"); //$NON-NLS-1$
sb.append("div.toc { display: inline; float: right; border: 1px solid "); //$NON-NLS-1$
appendCssColor(sb, borderColor);
sb.append("; }\n"); //$NON-NLS-1$
sb.append("span.mnemonic, div.toc a.mnemonic { text-decoration: underline; }\n"); //$NON-NLS-1$
sb.append("hr { border: 0; height: 1px; background: " ); //$NON-NLS-1$
appendCssColor(sb, borderColor);
sb.append("; }\n"); //$NON-NLS-1$
sb.append("}\n" ); // @media //$NON-NLS-1$
appendMatchColors(sb);
super.collectCss(sb);
}
}
public static class Info extends RHelpUIServlet {
private static final long serialVersionUID= 1L;
@Override
protected void collectCss(final StringBuilder sb) {
final Display display= Display.getCurrent();
final RGB infoForegroundColor= display.getSystemColor(SWT.COLOR_INFO_FOREGROUND).getRGB();
final RGB infoBackgroundColor= display.getSystemColor(SWT.COLOR_INFO_BACKGROUND).getRGB();
final int vIndent= Math.max(1, LayoutUtils.defaultVSpacing() / 4);
final int hIndent= Math.max(3, LayoutUtils.defaultHSpacing() / 2);
{ final FontDescriptor docFontDescr= JFaceResources.getDialogFontDescriptor();
final FontData fontData= docFontDescr.getFontData()[0];
sb.append("body { font-family: '"); //$NON-NLS-1$
sb.append(fontData.getName());
sb.append("'; font-size: "); //$NON-NLS-1$
sb.append(fontData.getHeight());
sb.append("pt; color: "); //$NON-NLS-1$
appendCssColor(sb, infoForegroundColor);
sb.append("; background: "); //$NON-NLS-1$
appendCssColor(sb, infoBackgroundColor);
sb.append("; margin: 0 "); //$NON-NLS-1$
sb.append(hIndent);
sb.append("px "); //$NON-NLS-1$
sb.append(vIndent);
sb.append("px; }\n"); //$NON-NLS-1$
}
appendELinkColors(sb, infoForegroundColor);
sb.append("h2, h3#description { display: none; }\n"); //$NON-NLS-1$
sb.append("h3 { font-size: 90%; margin-bottom: 0.4em; }\n"); //$NON-NLS-1$
sb.append("p, pre { margin-top: 0.4em; margin-bottom: 0.4em; }\n" ); //$NON-NLS-1$
sb.append("hr { visibility: hidden; }\n"); //$NON-NLS-1$
super.collectCss(sb);
}
}
private volatile @Nullable String cssStyle;
private RHelpRCodeScanner rCodeScanner;
@SuppressWarnings("null")
public RHelpUIServlet() {
}
@Override
public void init(final ServletConfig config) throws ServletException {
super.init(config);
init(RCore.getRHelpManager(),
JettyRHelpUtils.newResourceHandler(config.getServletContext()),
new JettyForwardHandler() );
this.rCodeScanner= new RHelpRCodeScanner(RUIPlugin.getInstance().getEditorPreferenceStore());
EditorsUI.getPreferenceStore().addPropertyChangeListener(this);
JFaceResources.getFontRegistry().addListener(this);
JFaceResources.getColorRegistry().addListener(this);
PreferencesUtil.getSettingsChangeNotifier().addChangeListener(this);
updateStyles();
}
@Override
public void propertyChange(final PropertyChangeEvent event) {
if (event.getProperty().equals("org.eclipse.statet.workbench.themes.DocViewFont") //$NON-NLS-1$
|| event.getProperty().equals("org.eclipse.statet.workbench.themes.DocViewBackgroundColor") //$NON-NLS-1$
|| event.getProperty().equals("org.eclipse.statet.workbench.themes.DocViewColor") //$NON-NLS-1$
|| event.getProperty().equals(JFaceResources.DIALOG_FONT)
|| event.getProperty().equals(JFacePreferences.HYPERLINK_COLOR)
|| event.getProperty().equals(JFacePreferences.ACTIVE_HYPERLINK_COLOR)
|| event.getProperty().equals("searchResultIndicationColor") ) { //$NON-NLS-1$
updateStyles();
}
}
@Override
public void settingsChanged(final Set<String> groupIds) {
if (groupIds.contains(RUIPreferenceConstants.R.TS_GROUP_ID)
|| groupIds.contains(RIdentifierGroups.GROUP_ID)) {
final Map<String, Object> options= new HashMap<>();
synchronized (this.rCodeScanner) {
this.rCodeScanner.handleSettingsChanged(groupIds, options);
}
}
}
@Override
public void destroy() {
super.destroy();
final IPreferenceStore preferenceStore= EditorsUI.getPreferenceStore();
if (preferenceStore != null) {
preferenceStore.removePropertyChangeListener(this);
}
JFaceResources.getFontRegistry().removeListener(this);
JFaceResources.getColorRegistry().removeListener(this);
PreferencesUtil.getSettingsChangeNotifier().removeChangeListener(this);
}
@Override
protected boolean getShowInternal() {
return PreferenceUtils.getInstancePrefs().getPreferenceValue(
RHelpPreferences.SHOW_INTERNAL_ENABLED_PREF );
}
@Override
protected boolean canOpenFile(final String ext) {
return true;
}
@Override
protected void doOpenFile(final Path file) {
try {
final IFileStore fileStore= EFS.getLocalFileSystem().getStore(file.toUri());
UIAccess.getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
final IWorkbenchPage page= UIAccess.getActiveWorkbenchPage(true);
try {
IDE.openEditorOnFileStore(page, fileStore);
}
catch (final PartInitException e) {}
}
});
}
catch (final Exception e) {}
}
private void updateStyles() {
final StringBuilder sb= new StringBuilder(1024);
UIAccess.getDisplay().syncExec(new Runnable() {
@Override
public void run() {
collectCss(sb);
}
});
sb.append(".header { display: none; }"); //$NON-NLS-1$
this.cssStyle= sb.toString();
}
protected void collectCss(final StringBuilder sb) {
}
@Override
protected void customizeCss(final PrintWriter writer) {
// updateStyles();
writer.print(this.cssStyle);
}
@Override
protected void customizePageHtmlHeader(final HttpServletRequest req, final PrintWriter writer) {
customizeHtmlHeader(req, writer, true);
}
@Override
protected void customizeIndexHtmlHeader(final HttpServletRequest req, final PrintWriter writer) {
customizeHtmlHeader(req, writer, false);
}
protected void customizeHtmlHeader(final HttpServletRequest req, final PrintWriter writer,
final boolean page) {
writer.println("<script type=\"text/javascript\">/* <![CDATA[ */"); //$NON-NLS-1$
writer.println("function keyNavHandler(event) {"); //$NON-NLS-1$
writer.println("if (!event) event= window.event;"); //$NON-NLS-1$
writer.println("if (event.which) { key= event.which } else if (event.keyCode) { key= event.keyCode };"); //$NON-NLS-1$
writer.println("if (!event.ctrlKey && !event.altKey) { var anchor= 0;"); //$NON-NLS-1$
if (page) {
writer.println("if (key == 68) anchor= \"#description\"; " + //$NON-NLS-1$
"else if (key == 85) anchor= \"#usage\"; " + //$NON-NLS-1$
"else if (key == 65) anchor= \"#arguments\"; " + //$NON-NLS-1$
"else if (key == 73) anchor= \"#details\"; " + //$NON-NLS-1$
"else if (key == 86) anchor= \"#value\"; " + //$NON-NLS-1$
"else if (key == 79) anchor= \"#authors\"; " + //$NON-NLS-1$
"else if (key == 82) anchor= \"#references\"; " + //$NON-NLS-1$
"else if (key == 69) anchor= \"#examples\"; " + //$NON-NLS-1$
"else if (key == 83) anchor= \"#seealso\";"); //$NON-NLS-1$
}
else {
writer.println("if (key >= 65 && key <= 90) anchor= \"#idx\"+String.fromCharCode(key+32);"); //$NON-NLS-1$
}
writer.println("if (anchor) { window.location.hash= anchor; event.cancelBubble= true; return false; }"); //$NON-NLS-1$
writer.println("} return true; }"); //$NON-NLS-1$
writer.println("document.onkeydown= keyNavHandler;"); //$NON-NLS-1$
writer.println("/* ]]> */</script>"); //$NON-NLS-1$
if (HOVER_STYLE.equals(req.getParameter(STYLE_PARAM))) {
writer.println("<style type=\"text/css\">body { overflow: hidden; }</style>"); //$NON-NLS-1$
}
}
@Override
protected void printRCode(final PrintWriter writer, final String html) {
synchronized (this.rCodeScanner) {
this.rCodeScanner.setHtml(html);
writer.write("<span style=\""); //$NON-NLS-1$
writer.write(this.rCodeScanner.getDefaultStyle());
writer.write("\">"); //$NON-NLS-1$
IToken token;
int currentIdx= 0;
while (!(token= this.rCodeScanner.nextToken()).isEOF()) {
final String data= (String) token.getData();
if (data != null) {
final int tokenIdx= this.rCodeScanner.getTokenOffset();
if (tokenIdx > currentIdx) {
writer.write(html, currentIdx, tokenIdx-currentIdx);
}
writer.write("<span style=\""); //$NON-NLS-1$
writer.write(data);
writer.write("\">"); //$NON-NLS-1$
writer.write(html, tokenIdx, this.rCodeScanner.getTokenLength());
writer.write("</span>"); //$NON-NLS-1$
currentIdx= tokenIdx + this.rCodeScanner.getTokenLength();
}
}
writer.write(html, currentIdx, html.length()-currentIdx);
writer.write("</span>"); //$NON-NLS-1$
}
}
}