[markdown] restore ability to use JavaFX WebView to preview markdown files (IDEA-243740)

This commit reverts 26e1b1296d.

GitOrigin-RevId: c78b65d09f45ab17e7a6554458c799931e6f7198
This commit is contained in:
Nikolay Chashnikov
2020-06-16 18:52:36 +03:00
committed by intellij-monorepo-bot
parent 9012b5af3f
commit 4fb4d7bb64
8 changed files with 261 additions and 13 deletions
@@ -122,6 +122,7 @@ Includes the following features:</p>
</applicationListeners>
<extensions defaultExtensionNs="org.intellij.markdown">
<html.panel.provider implementation="org.intellij.plugins.markdown.ui.preview.javafx.JavaFxHtmlPanelProvider" />
<html.panel.provider implementation="org.intellij.plugins.markdown.ui.preview.jcef.JCEFHtmlPanelProvider" />
<fenceLanguageProvider implementation="org.intellij.plugins.markdown.extensions.plantuml.PlantUMLCodeFenceLanguageProvider"/>
<codeFencePluginGeneratingProvider implementation="org.intellij.plugins.markdown.extensions.plantuml.PlantUMLCodeGeneratingProvider"/>
@@ -72,6 +72,7 @@ markdown.cannot.resolve.anchor.in.file.error.message=Cannot resolve anchor in fi
markdown.navigate.to.header=Select Header To Navigate To
markdown.navigate.to.header.no.headers=Cannot find header to navigate to
markdown.navigate.to.header.group=Markdown headers group
markdown.settings.preview.plantUML.download=Install
markdown.settings.preview.plantUML.installed=PlantUML framework is installed and ready to use.
markdown.settings.preview.plantUML.download.success=PlantUML has been successfully installed!
@@ -122,4 +123,5 @@ action.org.intellij.plugins.markdown.ui.actions.styling.ToggleBoldAction.text=To
action.org.intellij.plugins.markdown.ui.actions.styling.ToggleBoldAction.description=Toggles bold mode on caret/selection
group.Markdown.Toolbar.Left.text=Markdown Editor Actions
dialog.message.uri.parsing.reports.error=URI ''{0}'' parsing reports the error: {1}
dialog.message.cannot.set.preview.panel.provider=Cannot set preview panel provider ({0}):\n{1}
dialog.message.tried.to.use.preview.panel.provider=Tried to use preview panel provider ({0}), but it is unavailable. Reverting to default.
@@ -5,6 +5,7 @@ import com.intellij.util.xmlb.annotations.Attribute;
import com.intellij.util.xmlb.annotations.Property;
import com.intellij.util.xmlb.annotations.Tag;
import org.intellij.plugins.markdown.ui.preview.MarkdownHtmlPanelProvider;
import org.intellij.plugins.markdown.ui.preview.javafx.JavaFxHtmlPanelProvider;
import org.intellij.plugins.markdown.ui.preview.jcef.JCEFHtmlPanelProvider;
import org.intellij.plugins.markdown.ui.split.SplitFileEditor;
import org.jetbrains.annotations.NotNull;
@@ -20,7 +21,7 @@ public final class MarkdownPreviewSettings {
@Property(surroundWithTag = false)
@NotNull
private MarkdownHtmlPanelProvider.ProviderInfo myHtmlPanelProviderInfo =
JBCefApp.isSupported() ? new JCEFHtmlPanelProvider().getProviderInfo() : new MarkdownHtmlPanelProvider.ProviderInfo("Unavailable", "Unavailable");
JBCefApp.isSupported() ? new JCEFHtmlPanelProvider().getProviderInfo() : new JavaFxHtmlPanelProvider().getProviderInfo();
@Attribute("UseGrayscaleRendering")
private boolean myUseGrayscaleRendering = true;
@@ -351,10 +351,12 @@ public class MarkdownSettingsForm implements MarkdownCssSettings.Holder, Markdow
}
@NotNull
private static MarkdownHtmlPanelProvider getDefaultProvider() {
MarkdownHtmlPanelProvider[] providers = MarkdownHtmlPanelProvider.getProviders();
if (providers.length > 0) return providers[0];
throw new RuntimeException("No providers are defined");
private static MarkdownHtmlPanelProvider getProvider(@SuppressWarnings("SameParameterValue") @NotNull String providerClass) {
for (MarkdownHtmlPanelProvider provider : MarkdownHtmlPanelProvider.getProviders()) {
if (isProviderOf(provider.getProviderInfo(), providerClass)) return provider;
}
throw new RuntimeException("Cannot find " + providerClass);
}
@NotNull
@@ -363,7 +365,7 @@ public class MarkdownSettingsForm implements MarkdownCssSettings.Holder, Markdow
return Objects.requireNonNull(myPreviewPanelModel.getSelected());
}
else {
return getDefaultProvider().getProviderInfo();
return getProvider(JAVA_FX_HTML_PANEL_PROVIDER).getProviderInfo();
}
}
@@ -13,8 +13,12 @@
// limitations under the License.
package org.intellij.plugins.markdown.ui.preview;
import com.intellij.CommonBundle;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.ui.Messages;
import com.intellij.util.xmlb.annotations.Attribute;
import org.intellij.plugins.markdown.MarkdownBundle;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
@@ -40,12 +44,18 @@ public abstract class MarkdownHtmlPanelProvider {
@NotNull
public static MarkdownHtmlPanelProvider createFromInfo(@NotNull ProviderInfo providerInfo) {
for (MarkdownHtmlPanelProvider provider : getProviders()) {
if (provider.getProviderInfo().getClassName().equals(providerInfo.getClassName())) {
return provider;
}
try {
return ((MarkdownHtmlPanelProvider)Class.forName(providerInfo.getClassName()).newInstance());
}
catch (Exception e) {
Messages.showMessageDialog(
MarkdownBundle.message("dialog.message.cannot.set.preview.panel.provider", providerInfo.getName(), e.getMessage()),
CommonBundle.getErrorTitle(),
Messages.getErrorIcon()
);
Logger.getInstance(MarkdownHtmlPanelProvider.class).error(e);
return getProviders()[0];
}
return getProviders()[0];
}
public static boolean hasAvailableProviders() {
@@ -12,7 +12,7 @@ import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;
import org.intellij.plugins.markdown.settings.MarkdownCssSettings;
import org.intellij.plugins.markdown.ui.preview.jcef.MarkdownJCEFHtmlPanel;
import org.intellij.plugins.markdown.ui.preview.javafx.MarkdownJavaFxHtmlPanel;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.ide.BuiltInServerManager;
@@ -94,7 +94,7 @@ public class PreviewStaticServer extends HttpRequestHandler {
if ("scripts".equals(contentType) && MarkdownHtmlPanel.SCRIPTS.contains(fileName)) {
sendResource(request,
context.channel(),
MarkdownJCEFHtmlPanel.class,
MarkdownJavaFxHtmlPanel.class,
fileName);
}
else if ("styles".equals(contentType) && MarkdownHtmlPanel.STYLES.contains(fileName)) {
@@ -0,0 +1,36 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.intellij.plugins.markdown.ui.preview.javafx;
import org.intellij.plugins.markdown.ui.preview.MarkdownHtmlPanel;
import org.intellij.plugins.markdown.ui.preview.MarkdownHtmlPanelProvider;
import org.jetbrains.annotations.NotNull;
public class JavaFxHtmlPanelProvider extends MarkdownHtmlPanelProvider {
@NotNull
@Override
public MarkdownHtmlPanel createHtmlPanel() {
return new MarkdownJavaFxHtmlPanel();
}
@NotNull
@Override
public AvailabilityInfo isAvailable() {
try {
if (Class.forName("javafx.scene.web.WebView", false, getClass().getClassLoader()) != null) {
return AvailabilityInfo.AVAILABLE;
}
}
catch (ClassNotFoundException ignored) {
}
return AvailabilityInfo.UNAVAILABLE;
}
@NotNull
@Override
public ProviderInfo getProviderInfo() {
return new ProviderInfo("JavaFX WebView", JavaFxHtmlPanelProvider.class.getName());
}
}
@@ -0,0 +1,196 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.intellij.plugins.markdown.ui.preview.javafx;
import com.intellij.notification.NotificationGroup;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.NotNullLazyValue;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.ui.javafx.JavaFxHtmlPanel;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.messages.MessageBusConnection;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker.State;
import javafx.scene.text.FontSmoothingType;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import netscape.javascript.JSObject;
import org.intellij.markdown.html.HtmlGenerator;
import org.intellij.plugins.markdown.MarkdownBundle;
import org.intellij.plugins.markdown.settings.MarkdownApplicationSettings;
import org.intellij.plugins.markdown.ui.preview.MarkdownHtmlPanel;
import org.intellij.plugins.markdown.ui.preview.PreviewStaticServer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class MarkdownJavaFxHtmlPanel extends JavaFxHtmlPanel implements MarkdownHtmlPanel {
private static final NotNullLazyValue<String> MY_SCRIPTING_LINES = new NotNullLazyValue<String>() {
@NotNull
@Override
protected String compute() {
return SCRIPTS.stream()
.map(s -> "<script src=\"" + PreviewStaticServer.getScriptUrl(s) + "\"></script>")
.reduce((s, s2) -> s + "\n" + s2)
.orElseGet(String::new);
}
};
private String @NotNull [] myCssUris = ArrayUtilRt.EMPTY_STRING_ARRAY;
@NotNull
private String myCSP = "";
@NotNull
private String myLastRawHtml = "";
@NotNull
private final ScrollPreservingListener myScrollPreservingListener = new ScrollPreservingListener();
@NotNull
private final BridgeSettingListener myBridgeSettingListener = new BridgeSettingListener();
public MarkdownJavaFxHtmlPanel() {
super();
runInPlatformWhenAvailable(() -> {
if (myWebView != null) {
updateFontSmoothingType(myWebView, MarkdownApplicationSettings.getInstance().getMarkdownPreviewSettings().isUseGrayscaleRendering());
}
});
subscribeForGrayscaleSetting();
}
@Override
protected void registerListeners(@NotNull WebEngine engine) {
engine.getLoadWorker().stateProperty().addListener(myBridgeSettingListener);
engine.getLoadWorker().stateProperty().addListener(myScrollPreservingListener);
}
private void subscribeForGrayscaleSetting() {
MessageBusConnection settingsConnection = ApplicationManager.getApplication().getMessageBus().connect(this);
MarkdownApplicationSettings.SettingsChangedListener settingsChangedListener =
new MarkdownApplicationSettings.SettingsChangedListener() {
@Override
public void beforeSettingsChanged(@NotNull final MarkdownApplicationSettings settings) {
runInPlatformWhenAvailable(() -> {
if (myWebView != null) {
updateFontSmoothingType(myWebView, settings.getMarkdownPreviewSettings().isUseGrayscaleRendering());
}
});
}
};
settingsConnection.subscribe(MarkdownApplicationSettings.SettingsChangedListener.TOPIC, settingsChangedListener);
}
private static void updateFontSmoothingType(@NotNull WebView view, boolean isGrayscale) {
final FontSmoothingType typeToSet;
if (isGrayscale) {
typeToSet = FontSmoothingType.GRAY;
}
else {
typeToSet = FontSmoothingType.LCD;
}
view.fontSmoothingTypeProperty().setValue(typeToSet);
}
@Override
public void setHtml(@NotNull String html) {
myLastRawHtml = html;
super.setHtml(html);
}
@NotNull
@Override
protected String prepareHtml(@NotNull String html) {
return ImageRefreshFix.setStamps(html
.replace("<head>", "<head>"
+ "<meta http-equiv=\"Content-Security-Policy\" content=\"" + myCSP + "\"/>"
+ MarkdownHtmlPanel.getCssLines(null, myCssUris) + "\n" + getScriptingLines()));
}
@Override
public void setCSS(@Nullable String inlineCss, String @NotNull ... fileUris) {
PreviewStaticServer.getInstance().setInlineStyle(inlineCss);
myCssUris = inlineCss == null ? fileUris
: ArrayUtil
.mergeArrays(fileUris, PreviewStaticServer.getStyleUrl(PreviewStaticServer.INLINE_CSS_FILENAME));
myCSP = PreviewStaticServer.createCSP(ContainerUtil.map(SCRIPTS, s -> PreviewStaticServer.getScriptUrl(s)),
ContainerUtil.concat(
ContainerUtil.map(STYLES, s -> PreviewStaticServer.getStyleUrl(s)),
ContainerUtil.filter(fileUris, s -> s.startsWith("http://") || s.startsWith("https://"))
));
setHtml(myLastRawHtml);
}
@Override
public void scrollToMarkdownSrcOffset(final int offset) {
runInPlatformWhenAvailable(() -> {
getWebViewGuaranteed().getEngine().executeScript(
"if ('__IntelliJTools' in window) " +
"__IntelliJTools.scrollToOffset(" + offset + ", '" + HtmlGenerator.Companion.getSRC_ATTRIBUTE_NAME() + "');"
);
final Object result = getWebViewGuaranteed().getEngine().executeScript(
"document.documentElement.scrollTop || (document.body && document.body.scrollTop)");
if (result instanceof Number) {
myScrollPreservingListener.myScrollY = ((Number)result).intValue();
}
});
}
@Override
public void dispose() {
runInPlatformWhenAvailable(() -> {
getWebViewGuaranteed().getEngine().getLoadWorker().stateProperty().removeListener(myScrollPreservingListener);
getWebViewGuaranteed().getEngine().getLoadWorker().stateProperty().removeListener(myBridgeSettingListener);
});
}
@NotNull
private static String getScriptingLines() {
return MY_SCRIPTING_LINES.getValue();
}
@SuppressWarnings("unused")
public static class JavaPanelBridge {
static final JavaPanelBridge INSTANCE = new JavaPanelBridge();
private static final NotificationGroup MARKDOWN_NOTIFICATION_GROUP = NotificationGroup
.toolWindowGroup("Markdown headers group", ToolWindowId.MESSAGES_WINDOW, true,
MarkdownBundle.message("markdown.navigate.to.header.group"));
public void openInExternalBrowser(@NotNull String link) {
SafeOpener.openLink(link);
}
public void log(@Nullable String text) {
Logger.getInstance(JavaPanelBridge.class).warn(text);
}
}
private class BridgeSettingListener implements ChangeListener<State> {
@Override
public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue) {
JSObject win
= (JSObject)getWebViewGuaranteed().getEngine().executeScript("window");
win.setMember("JavaPanelBridge", JavaPanelBridge.INSTANCE);
}
}
private class ScrollPreservingListener implements ChangeListener<State> {
volatile int myScrollY = 0;
@Override
public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue) {
if (newValue == State.RUNNING) {
final Object result =
getWebViewGuaranteed().getEngine().executeScript("document.documentElement.scrollTop || document.body.scrollTop");
if (result instanceof Number) {
myScrollY = ((Number)result).intValue();
}
}
else if (newValue == State.SUCCEEDED) {
getWebViewGuaranteed().getEngine()
.executeScript("document.documentElement.scrollTop = ({} || document.body).scrollTop = " + myScrollY);
}
}
}
}