diff --git a/python/ipnb/ipnb.iml b/python/ipnb/ipnb.iml index bd96a0aa04a7..4fd2c2691fe1 100644 --- a/python/ipnb/ipnb.iml +++ b/python/ipnb/ipnb.iml @@ -25,18 +25,5 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbJfxUtils.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbJfxUtils.java new file mode 100644 index 000000000000..d1504ad438f8 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbJfxUtils.java @@ -0,0 +1,250 @@ +package org.jetbrains.plugins.ipnb; + +import com.intellij.ide.BrowserUtil; +import com.intellij.ide.ui.LafManager; +import com.intellij.ide.ui.laf.darcula.DarculaLookAndFeelInfo; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.text.MarkdownUtil; +import com.intellij.util.ui.UIUtil; +import javafx.application.Platform; +import javafx.concurrent.Worker; +import javafx.embed.swing.JFXPanel; +import javafx.event.EventHandler; +import javafx.scene.Scene; +import javafx.scene.layout.BorderPane; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebEvent; +import javafx.scene.web.WebView; +import netscape.javascript.JSException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.IpnbEditorUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class IpnbJfxUtils { + private static final Logger LOG = Logger.getInstance(IpnbJfxUtils.class); + private static final String ourPrefix = "
"; + + private static final String ourPostfix = "
"; + private static URL ourStyleUrl; + + public static JComponent createHtmlPanel(@NotNull final String source, int width) { + + final JFXPanel javafxPanel = new JFXPanel(){ + @Override + protected void processMouseWheelEvent(MouseWheelEvent e) { + final Container parent = getParent(); + final MouseEvent parentEvent = SwingUtilities.convertMouseEvent(this, e, parent); + parent.dispatchEvent(parentEvent); + } + }; + javafxPanel.setBackground(IpnbEditorUtil.getBackground()); + + Platform.runLater(() -> { + final WebView webView = new WebView(); + webView.setOnDragDetected(new EventHandler() { + @Override + public void handle(javafx.scene.input.MouseEvent event) { + } + }); + final WebEngine engine = webView.getEngine(); + initHyperlinkListener(engine); + engine.setOnStatusChanged(new EventHandler>() { + public void handle(WebEvent status) { + adjustHeight(webView, javafxPanel, source); + } + }); + + final String prefix = String.format(ourPrefix, EditorColorsManager.getInstance().getGlobalScheme().getEditorFontSize()); + engine.loadContent(prefix + convertToHtml(source) + ourPostfix); + final BorderPane pane = new BorderPane(webView); + final Scene scene = new Scene(pane, width != 0 ? width : 20, 20); + javafxPanel.setScene(scene); + Platform.runLater(() -> adjustHeight(webView, javafxPanel, source)); + updateLaf(LafManager.getInstance().getCurrentLookAndFeel() instanceof DarculaLookAndFeelInfo, + pane, engine, javafxPanel); + }); + + return javafxPanel; + } + + private static String convertToHtml(@NotNull String source) { + source = StringUtil.replace(source, "class=\"alert alert-success\"", "class=\"alert-success\""); + source = StringUtil.replace(source, "class=\"alert alert-error\"", "class=\"alert-error\""); + ArrayList lines = ContainerUtil.newArrayList(source.split("\n|\r|\r\n")); + + MarkdownUtil.replaceHeaders(lines); + source = StringUtil.join(lines, "\n"); + final StringBuilder result = new StringBuilder(); + + source = replaceLinks(source); + + boolean escaped = false; + int start = 0; + int end = StringUtil.indexOf(source, "```"); + while (end > 0) { + result.append(source.substring(start, end)); + result.append(escaped? "" : "
");
+      escaped = !escaped;
+      start = end + 3;
+      end = StringUtil.indexOf(source, "```", end + 1);
+    }
+    result.append(source.substring(start));
+
+    return result.toString();
+  }
+
+  @NotNull
+  private static String replaceLinks(@NotNull String source) {
+    final Pattern inlineLink = Pattern.compile("(\\[(.*?)\\]\\([ \\t]*?[ \\t]*(([\'\"])(.*?)\\5)?\\))", 32);
+    final Matcher matcher = inlineLink.matcher(source);
+    final StringBuffer sb = new StringBuffer();
+    while (matcher.find()) {
+      String linkText = matcher.group(2);
+      String url = matcher.group(3);
+      String title = matcher.group(6);
+      StringBuilder link = new StringBuilder();
+      link.append("").append(linkText);
+      link.append("");
+      matcher.appendReplacement(sb, link.toString());
+    }
+    matcher.appendTail(sb);
+
+    source = sb.toString();
+    return source;
+  }
+
+  private static void initHyperlinkListener(@NotNull final WebEngine engine) {
+    engine.getLoadWorker().stateProperty().addListener((ov, oldState, newState) -> {
+      if (newState == Worker.State.SUCCEEDED) {
+        final EventListener listener = new HyperlinkListener(engine);
+        addListenerToAllHyperlinkItems(engine, listener);
+      }
+    });
+  }
+
+  private static void addListenerToAllHyperlinkItems(WebEngine engine, EventListener listener) {
+    final Document doc = engine.getDocument();
+    if (doc != null) {
+      final NodeList nodeList = doc.getElementsByTagName("a");
+      for (int i = 0; i < nodeList.getLength(); i++) {
+        ((EventTarget)nodeList.item(i)).addEventListener("click", listener, false);
+      }
+    }
+  }
+
+  private static class HyperlinkListener implements EventListener {
+    @NotNull private final WebEngine myEngine;
+
+    public HyperlinkListener(@NotNull final WebEngine engine) {
+      myEngine = engine;
+    }
+
+    @Override
+    public void handleEvent(Event ev) {
+      String domEventType = ev.getType();
+      if (domEventType.equals("click")) {
+        myEngine.setJavaScriptEnabled(true);
+        myEngine.getLoadWorker().cancel();
+        ev.preventDefault();
+
+        UIUtil.invokeLaterIfNeeded(() -> {
+
+          final String href = ((Element)ev.getTarget()).getAttribute("href");
+          if (href == null) return;
+          final URI address;
+          try {
+            address = new URI(href);
+            BrowserUtil.browse(address);
+          }
+          catch (URISyntaxException e) {
+            LOG.warn(e.getMessage());
+          }
+        });
+
+      }
+    }
+  }
+
+  private static void adjustHeight(final WebView webView, final JFXPanel javafxPanel, String source) {
+    try {
+      Object result = webView.getEngine().executeScript("document.getElementById(\"mydiv\").offsetHeight");
+      if (result instanceof Integer) {
+        final int fontSize = EditorColorsManager.getInstance().getGlobalScheme().getEditorFontSize();
+        double x = (double)source.length() * 8 / (int)result;
+        final double height = (source.length() * fontSize) / x + 20;
+        final int width = (int)(webView.getWidth() == 0 ? 1500 : webView.getWidth());
+        final Dimension size = new Dimension(width, (int)height);
+
+        UIUtil.invokeLaterIfNeeded(new Runnable() {
+          @Override
+          public void run() {
+            javafxPanel.setPreferredSize(size);
+            javafxPanel.revalidate();
+            javafxPanel.repaint();
+          }
+        });
+      }
+    }
+    catch (JSException ignore) {
+    }
+  }
+
+  private static void updateLaf(boolean isDarcula, BorderPane pane, WebEngine engine, JFXPanel jfxPanel) {
+    if (isDarcula) {
+      updateLafDarcula(pane, engine, jfxPanel);
+    }
+  }
+
+  private static void updateLafDarcula(BorderPane pane, WebEngine engine, JFXPanel jfxPanel) {
+    Platform.runLater(() -> {
+      ourStyleUrl = IpnbFileType.class.getResource("/style/javaFXBrowserDarcula.css");
+      engine.setUserStyleSheetLocation(ourStyleUrl.toExternalForm());
+      pane.setStyle("-fx-background-color: #313335");
+      jfxPanel.getScene().getStylesheets().add(ourStyleUrl.toExternalForm());
+      engine.reload();
+    });
+  }
+}
diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbUtils.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbUtils.java
index c21a79384f6a..c097a0845ee8 100644
--- a/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbUtils.java
+++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbUtils.java
@@ -1,68 +1,17 @@
 package org.jetbrains.plugins.ipnb;
 
-import com.intellij.ide.BrowserUtil;
-import com.intellij.ide.ui.LafManager;
-import com.intellij.ide.ui.laf.darcula.DarculaLookAndFeelInfo;
-import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.editor.colors.EditorColorsManager;
-import com.intellij.openapi.util.text.StringUtil;
-import com.intellij.util.containers.ContainerUtil;
-import com.intellij.util.text.MarkdownUtil;
-import com.intellij.util.ui.UIUtil;
+import com.intellij.ui.JBColor;
 import javafx.application.Platform;
-import javafx.concurrent.Worker;
-import javafx.embed.swing.JFXPanel;
-import javafx.event.EventHandler;
-import javafx.scene.Scene;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.web.WebEngine;
-import javafx.scene.web.WebEvent;
-import javafx.scene.web.WebView;
-import netscape.javascript.JSException;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.plugins.ipnb.editor.IpnbEditorUtil;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.NodeList;
-import org.w3c.dom.events.Event;
-import org.w3c.dom.events.EventListener;
-import org.w3c.dom.events.EventTarget;
 
 import javax.swing.*;
 import java.awt.*;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
-import java.awt.event.MouseWheelEvent;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 public class IpnbUtils {
-  private static final Logger LOG = Logger.getInstance(IpnbUtils.class);
-  private static final String ourPrefix = "
"; - - private static final String ourPostfix = "
"; - private static URL ourStyleUrl; + private static int hasFx = 0; public static JComponent createLatexPane(@NotNull final String source, int width) { final JComponent panel = createHtmlPanel(source, width); @@ -80,189 +29,33 @@ public class IpnbUtils { return panel; } + public static boolean hasFx() { + if (hasFx == 0) { + try { + Platform.setImplicitExit(false); + hasFx = 1; + } + catch (NoClassDefFoundError e) { + hasFx = 2; + } + } + return hasFx == 1; + } + public static JComponent createHtmlPanel(@NotNull final String source, int width) { - Platform.setImplicitExit(false); - - final JFXPanel javafxPanel = new JFXPanel(){ - @Override - protected void processMouseWheelEvent(MouseWheelEvent e) { - final Container parent = getParent(); - final MouseEvent parentEvent = SwingUtilities.convertMouseEvent(this, e, parent); - parent.dispatchEvent(parentEvent); - } - }; - javafxPanel.setBackground(IpnbEditorUtil.getBackground()); - - Platform.runLater(() -> { - final WebView webView = new WebView(); - webView.setOnDragDetected(new EventHandler() { - @Override - public void handle(javafx.scene.input.MouseEvent event) { - } - }); - final WebEngine engine = webView.getEngine(); - initHyperlinkListener(engine); - engine.setOnStatusChanged(new EventHandler>() { - public void handle(WebEvent status) { - adjustHeight(webView, javafxPanel, source); - } - }); - - final String prefix = String.format(ourPrefix, EditorColorsManager.getInstance().getGlobalScheme().getEditorFontSize()); - engine.loadContent(prefix + convertToHtml(source) + ourPostfix); - final BorderPane pane = new BorderPane(webView); - final Scene scene = new Scene(pane, width != 0 ? width : 20, 20); - javafxPanel.setScene(scene); - Platform.runLater(() -> adjustHeight(webView, javafxPanel, source)); - updateLaf(LafManager.getInstance().getCurrentLookAndFeel() instanceof DarculaLookAndFeelInfo, - pane, engine, javafxPanel); - }); - - return javafxPanel; - } - - private static String convertToHtml(@NotNull String source) { - source = StringUtil.replace(source, "class=\"alert alert-success\"", "class=\"alert-success\""); - source = StringUtil.replace(source, "class=\"alert alert-error\"", "class=\"alert-error\""); - ArrayList lines = ContainerUtil.newArrayList(source.split("\n|\r|\r\n")); - - MarkdownUtil.replaceHeaders(lines); - source = StringUtil.join(lines, "\n"); - final StringBuilder result = new StringBuilder(); - - source = replaceLinks(source); - - boolean escaped = false; - int start = 0; - int end = StringUtil.indexOf(source, "```"); - while (end > 0) { - result.append(source.substring(start, end)); - result.append(escaped? "
" : "
");
-      escaped = !escaped;
-      start = end + 3;
-      end = StringUtil.indexOf(source, "```", end + 1);
+    if (hasFx()) {
+      return IpnbJfxUtils.createHtmlPanel(source, width);
     }
-    result.append(source.substring(start));
-
-    return result.toString();
+    return createNonJfxPanel(source);
   }
 
-  @NotNull
-  private static String replaceLinks(@NotNull String source) {
-    final Pattern inlineLink = Pattern.compile("(\\[(.*?)\\]\\([ \\t]*?[ \\t]*(([\'\"])(.*?)\\5)?\\))", 32);
-    final Matcher matcher = inlineLink.matcher(source);
-    final StringBuffer sb = new StringBuffer();
-    while (matcher.find()) {
-      String linkText = matcher.group(2);
-      String url = matcher.group(3);
-      String title = matcher.group(6);
-      StringBuilder link = new StringBuilder();
-      link.append("").append(linkText);
-      link.append("");
-      matcher.appendReplacement(sb, link.toString());
-    }
-    matcher.appendTail(sb);
-
-    source = sb.toString();
-    return source;
+  public static JComponent createNonJfxPanel(@NotNull final String source) {
+    final JTextArea textArea = new JTextArea(source);
+    textArea.setLineWrap(true);
+    textArea.setEditable(false);
+    textArea.setBorder(BorderFactory.createLineBorder(JBColor.lightGray));
+    textArea.setBackground(IpnbEditorUtil.getBackground());
+    return textArea;
   }
 
-  private static void initHyperlinkListener(@NotNull final WebEngine engine) {
-    engine.getLoadWorker().stateProperty().addListener((ov, oldState, newState) -> {
-      if (newState == Worker.State.SUCCEEDED) {
-        final EventListener listener = new HyperlinkListener(engine);
-        addListenerToAllHyperlinkItems(engine, listener);
-      }
-    });
-  }
-
-  private static void addListenerToAllHyperlinkItems(WebEngine engine, EventListener listener) {
-    final Document doc = engine.getDocument();
-    if (doc != null) {
-      final NodeList nodeList = doc.getElementsByTagName("a");
-      for (int i = 0; i < nodeList.getLength(); i++) {
-        ((EventTarget)nodeList.item(i)).addEventListener("click", listener, false);
-      }
-    }
-  }
-
-  private static class HyperlinkListener implements EventListener {
-    @NotNull private final WebEngine myEngine;
-
-    public HyperlinkListener(@NotNull final WebEngine engine) {
-      myEngine = engine;
-    }
-
-    @Override
-    public void handleEvent(Event ev) {
-      String domEventType = ev.getType();
-      if (domEventType.equals("click")) {
-        myEngine.setJavaScriptEnabled(true);
-        myEngine.getLoadWorker().cancel();
-        ev.preventDefault();
-
-        UIUtil.invokeLaterIfNeeded(() -> {
-
-          final String href = ((Element)ev.getTarget()).getAttribute("href");
-          if (href == null) return;
-          final URI address;
-          try {
-            address = new URI(href);
-            BrowserUtil.browse(address);
-          }
-          catch (URISyntaxException e) {
-            LOG.warn(e.getMessage());
-          }
-        });
-
-      }
-    }
-  }
-
-  private static void adjustHeight(final WebView webView, final JFXPanel javafxPanel, String source) {
-    try {
-      Object result = webView.getEngine().executeScript("document.getElementById(\"mydiv\").offsetHeight");
-      if (result instanceof Integer) {
-        final int fontSize = EditorColorsManager.getInstance().getGlobalScheme().getEditorFontSize();
-        double x = (double)source.length() * 8 / (int)result;
-        final double height = (source.length() * fontSize) / x + 20;
-        final int width = (int)(webView.getWidth() == 0 ? 1500 : webView.getWidth());
-        final Dimension size = new Dimension(width, (int)height);
-
-        UIUtil.invokeLaterIfNeeded(new Runnable() {
-          @Override
-          public void run() {
-            javafxPanel.setPreferredSize(size);
-            javafxPanel.revalidate();
-            javafxPanel.repaint();
-          }
-        });
-      }
-    }
-    catch (JSException ignore) {
-    }
-  }
-
-  private static void updateLaf(boolean isDarcula, BorderPane pane, WebEngine engine, JFXPanel jfxPanel) {
-    if (isDarcula) {
-      updateLafDarcula(pane, engine, jfxPanel);
-    }
-  }
-
-  private static void updateLafDarcula(BorderPane pane, WebEngine engine, JFXPanel jfxPanel) {
-    Platform.runLater(() -> {
-      ourStyleUrl = IpnbFileType.class.getResource("/style/javaFXBrowserDarcula.css");
-      engine.setUserStyleSheetLocation(ourStyleUrl.toExternalForm());
-      pane.setStyle("-fx-background-color: #313335");
-      jfxPanel.getScene().getStylesheets().add(ourStyleUrl.toExternalForm());
-      engine.reload();
-    });
-  }
 }
diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbFilePanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbFilePanel.java
index 61a98e370309..8351146618bb 100644
--- a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbFilePanel.java
+++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbFilePanel.java
@@ -22,6 +22,7 @@ import com.intellij.util.Alarm;
 import com.intellij.util.ui.UIUtil;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.ipnb.IpnbUtils;
 import org.jetbrains.plugins.ipnb.editor.IpnbEditorUtil;
 import org.jetbrains.plugins.ipnb.editor.IpnbFileEditor;
 import org.jetbrains.plugins.ipnb.editor.actions.IpnbCutCellAction;
@@ -134,6 +135,7 @@ public class IpnbFilePanel extends JPanel implements Scrollable, DataProvider, D
   }
 
   private void layoutFile() {
+    addWarningIfNeeded();
     final List cells = myIpnbFile.getCells();
     for (IpnbCell cell : cells) {
       addCellToPanel(cell);
@@ -157,6 +159,13 @@ public class IpnbFilePanel extends JPanel implements Scrollable, DataProvider, D
     });
   }
 
+  private void addWarningIfNeeded() {
+    if (IpnbUtils.hasFx()) return;
+    final JLabel warning = new JLabel("Switch to the bundled JDK for proper markdown cell rendering", SwingConstants.CENTER);
+    warning.setForeground(JBColor.RED);
+    add(warning);
+  }
+
   private void addCellToPanel(IpnbCell cell) {
     IpnbEditablePanel panel;
     if (cell instanceof IpnbCodeCell) {