From 4fb4d7bb64a8ae96a2984cfc4d2734274b8fff55 Mon Sep 17 00:00:00 2001
From: Nikolay Chashnikov
Date: Tue, 16 Jun 2020 18:14:40 +0300
Subject: [PATCH] [markdown] restore ability to use JavaFX WebView to preview
markdown files (IDEA-243740)
This commit reverts 26e1b1296d.
GitOrigin-RevId: c78b65d09f45ab17e7a6554458c799931e6f7198
---
plugins/markdown/resource/META-INF/plugin.xml | 1 +
.../messages/MarkdownBundle.properties | 2 +
.../settings/MarkdownPreviewSettings.java | 3 +-
.../settings/MarkdownSettingsForm.java | 12 +-
.../ui/preview/MarkdownHtmlPanelProvider.java | 20 +-
.../ui/preview/PreviewStaticServer.java | 4 +-
.../javafx/JavaFxHtmlPanelProvider.java | 36 ++++
.../javafx/MarkdownJavaFxHtmlPanel.java | 196 ++++++++++++++++++
8 files changed, 261 insertions(+), 13 deletions(-)
create mode 100644 plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/javafx/JavaFxHtmlPanelProvider.java
create mode 100644 plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/javafx/MarkdownJavaFxHtmlPanel.java
diff --git a/plugins/markdown/resource/META-INF/plugin.xml b/plugins/markdown/resource/META-INF/plugin.xml
index 7c080a55f8a4..212f389fb8b7 100644
--- a/plugins/markdown/resource/META-INF/plugin.xml
+++ b/plugins/markdown/resource/META-INF/plugin.xml
@@ -122,6 +122,7 @@ Includes the following features:
+
diff --git a/plugins/markdown/resource/messages/MarkdownBundle.properties b/plugins/markdown/resource/messages/MarkdownBundle.properties
index 856b83ad97bf..a314e5402d33 100644
--- a/plugins/markdown/resource/messages/MarkdownBundle.properties
+++ b/plugins/markdown/resource/messages/MarkdownBundle.properties
@@ -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.
\ No newline at end of file
diff --git a/plugins/markdown/src/org/intellij/plugins/markdown/settings/MarkdownPreviewSettings.java b/plugins/markdown/src/org/intellij/plugins/markdown/settings/MarkdownPreviewSettings.java
index 15154112361c..ff2ea0572619 100644
--- a/plugins/markdown/src/org/intellij/plugins/markdown/settings/MarkdownPreviewSettings.java
+++ b/plugins/markdown/src/org/intellij/plugins/markdown/settings/MarkdownPreviewSettings.java
@@ -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;
diff --git a/plugins/markdown/src/org/intellij/plugins/markdown/settings/MarkdownSettingsForm.java b/plugins/markdown/src/org/intellij/plugins/markdown/settings/MarkdownSettingsForm.java
index 54ab39fbeee2..f05ee8b131f2 100644
--- a/plugins/markdown/src/org/intellij/plugins/markdown/settings/MarkdownSettingsForm.java
+++ b/plugins/markdown/src/org/intellij/plugins/markdown/settings/MarkdownSettingsForm.java
@@ -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();
}
}
diff --git a/plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/MarkdownHtmlPanelProvider.java b/plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/MarkdownHtmlPanelProvider.java
index 8fc0da06d6ad..ab983d565ac6 100644
--- a/plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/MarkdownHtmlPanelProvider.java
+++ b/plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/MarkdownHtmlPanelProvider.java
@@ -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() {
diff --git a/plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/PreviewStaticServer.java b/plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/PreviewStaticServer.java
index 6f6c9a725413..837d77f2bf7b 100644
--- a/plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/PreviewStaticServer.java
+++ b/plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/PreviewStaticServer.java
@@ -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)) {
diff --git a/plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/javafx/JavaFxHtmlPanelProvider.java b/plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/javafx/JavaFxHtmlPanelProvider.java
new file mode 100644
index 000000000000..15e0dd917e76
--- /dev/null
+++ b/plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/javafx/JavaFxHtmlPanelProvider.java
@@ -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());
+ }
+
+}
diff --git a/plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/javafx/MarkdownJavaFxHtmlPanel.java b/plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/javafx/MarkdownJavaFxHtmlPanel.java
new file mode 100644
index 000000000000..f60ca97712b1
--- /dev/null
+++ b/plugins/markdown/src/org/intellij/plugins/markdown/ui/preview/javafx/MarkdownJavaFxHtmlPanel.java
@@ -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 MY_SCRIPTING_LINES = new NotNullLazyValue() {
+ @NotNull
+ @Override
+ protected String compute() {
+ return SCRIPTS.stream()
+ .map(s -> "")
+ .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("", ""
+ + ""
+ + 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 {
+ @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 {
+ 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);
+ }
+ }
+ }
+}