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 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 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); + } + } + } +}