790725070af877560f720be85ea51f25e68c7406
[idea/community.git] / python / educational-core / student / src / com / jetbrains / edu / learning / ui / StudyBrowserWindow.java
1 package com.jetbrains.edu.learning.ui;
2
3 import com.intellij.icons.AllIcons;
4 import com.intellij.ide.BrowserUtil;
5 import com.intellij.ide.ui.LafManager;
6 import com.intellij.ide.ui.LafManagerListener;
7 import com.intellij.ide.ui.laf.darcula.DarculaLookAndFeelInfo;
8 import com.intellij.openapi.application.ApplicationManager;
9 import com.intellij.openapi.diagnostic.Logger;
10 import com.intellij.openapi.editor.colors.EditorColorsManager;
11 import com.intellij.openapi.editor.colors.EditorColorsScheme;
12 import com.intellij.openapi.util.io.StreamUtil;
13 import com.jetbrains.edu.learning.StudyPluginConfigurator;
14 import javafx.application.Platform;
15 import javafx.concurrent.Worker;
16 import javafx.embed.swing.JFXPanel;
17 import javafx.scene.Scene;
18 import javafx.scene.control.ProgressBar;
19 import javafx.scene.layout.StackPane;
20 import javafx.scene.web.WebEngine;
21 import javafx.scene.web.WebHistory;
22 import javafx.scene.web.WebView;
23 import org.jetbrains.annotations.NotNull;
24 import org.jetbrains.annotations.Nullable;
25 import org.w3c.dom.*;
26 import org.w3c.dom.events.Event;
27 import org.w3c.dom.events.EventListener;
28 import org.w3c.dom.events.EventTarget;
29
30 import javax.swing.*;
31 import java.awt.*;
32 import java.awt.event.MouseAdapter;
33 import java.awt.event.MouseEvent;
34 import java.io.IOException;
35 import java.io.InputStream;
36 import java.net.URL;
37
38 class StudyBrowserWindow extends JFrame {
39   private static final Logger LOG = Logger.getInstance(StudyToolWindow.class);
40   private static final String EVENT_TYPE_CLICK = "click";
41   private JFXPanel myPanel;
42   private WebView myWebComponent;
43   private StackPane myPane;
44
45   private WebEngine myEngine;
46   private ProgressBar myProgressBar;
47   private boolean myLinkInNewBrowser = true;
48   private boolean myShowProgress = false;
49
50   public StudyBrowserWindow(final boolean linkInNewWindow, final boolean showProgress) {
51     myLinkInNewBrowser = linkInNewWindow;
52     myShowProgress = showProgress;
53     setSize(new Dimension(900, 800));
54     setLayout(new BorderLayout());
55     setPanel(new JFXPanel());
56     setTitle("Study Browser");
57     LafManager.getInstance().addLafManagerListener(new StudyLafManagerListener());
58     initComponents();
59   }
60
61   private void updateLaf(boolean isDarcula) {
62     if (isDarcula) {
63       updateLafDarcula();
64     }
65     else {
66       updateIntellijAndGTKLaf();
67     }
68   }
69
70   private void updateIntellijAndGTKLaf() {
71     Platform.runLater(() -> {
72       final URL scrollBarStyleUrl = getClass().getResource("/style/javaFXBrowserScrollBar.css");
73       myPane.getStylesheets().add(scrollBarStyleUrl.toExternalForm());
74       myEngine.setUserStyleSheetLocation(null);
75       myEngine.reload();
76     });
77   }
78
79   private void updateLafDarcula() {
80     Platform.runLater(() -> {
81       final URL engineStyleUrl = getClass().getResource("/style/javaFXBrowserDarcula.css");
82       final URL scrollBarStyleUrl = getClass().getResource("/style/javaFXBrowserDarculaScrollBar.css");
83       myEngine.setUserStyleSheetLocation(engineStyleUrl.toExternalForm());
84       myPane.getStylesheets().add(scrollBarStyleUrl.toExternalForm());
85       myPane.setStyle("-fx-background-color: #3c3f41");
86       myPanel.getScene().getStylesheets().add(engineStyleUrl.toExternalForm());
87       myEngine.reload();
88     });
89   }
90
91   private void initComponents() {
92     Platform.runLater(() -> {
93       myPane = new StackPane();
94       myWebComponent = new WebView();
95       myEngine = myWebComponent.getEngine();
96
97
98       if (myShowProgress) {
99         myProgressBar = makeProgressBarWithListener();
100         myWebComponent.setVisible(false);
101         myPane.getChildren().addAll(myWebComponent, myProgressBar);
102       }
103       else {
104         myPane.getChildren().add(myWebComponent);
105       }
106       if (myLinkInNewBrowser) {
107         initHyperlinkListener();
108       }
109       Scene scene = new Scene(myPane);
110       myPanel.setScene(scene);
111       myPanel.setVisible(true);
112       updateLaf(LafManager.getInstance().getCurrentLookAndFeel() instanceof DarculaLookAndFeelInfo);
113     });
114
115     add(myPanel, BorderLayout.CENTER);
116     setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
117   }
118
119
120   public void load(@NotNull final String url) {
121     Platform.runLater(() -> {
122       updateLookWithProgressBarIfNeeded();
123       myEngine.load(url);
124     });
125   }
126
127   public void loadContent(@NotNull final String content, @Nullable StudyPluginConfigurator configurator) {
128     if (configurator == null) {
129       Platform.runLater(() -> myEngine.loadContent("Seems like desired plugin doesn't installed"));
130       LOG.warn("No StudyPluginConfigurator is provided for the plugin");
131     }
132     else {
133       String withCodeHighlighting = createHtmlWithCodeHighlighting(content, configurator);
134       Platform.runLater(() -> {
135         updateLookWithProgressBarIfNeeded();
136         myEngine.loadContent(withCodeHighlighting);
137       });
138     }
139   }
140
141   @Nullable
142   private String createHtmlWithCodeHighlighting(@NotNull final String content, @NotNull StudyPluginConfigurator configurator) {
143     String template = null;
144     InputStream stream = getClass().getResourceAsStream("/code-mirror/template.html");
145     try {
146       template = StreamUtil.readText(stream, "utf-8");
147     }
148     catch (IOException e) {
149       LOG.warn(e.getMessage());
150     }
151     finally {
152       try {
153         stream.close();
154       }
155       catch (IOException e) {
156         LOG.warn(e.getMessage());
157       }
158     }
159
160     if (template == null) {
161       LOG.warn("Code mirror template is null");
162       return null;
163     }
164
165     final EditorColorsScheme editorColorsScheme = EditorColorsManager.getInstance().getGlobalScheme();
166     int fontSize = editorColorsScheme.getEditorFontSize();
167     
168     template = template.replace("${font_size}", String.valueOf(fontSize- 2));
169     template = template.replace("${codemirror}", getClass().getResource("/code-mirror/codemirror.js").toExternalForm());
170     template = template.replace("${language_script}", configurator.getLanguageScriptUrl());
171     template = template.replace("${default_mode}", configurator.getDefaultHighlightingMode());
172     template = template.replace("${runmode}", getClass().getResource("/code-mirror/runmode.js").toExternalForm());
173     template = template.replace("${colorize}", getClass().getResource("/code-mirror/colorize.js").toExternalForm());
174     template = template.replace("${javascript}", getClass().getResource("/code-mirror/javascript.js").toExternalForm());
175     if (LafManager.getInstance().getCurrentLookAndFeel() instanceof DarculaLookAndFeelInfo) {
176       template = template.replace("${css_oldcodemirror}", getClass().getResource("/code-mirror/codemirror-old-darcula.css").toExternalForm());
177       template = template.replace("${css_codemirror}", getClass().getResource("/code-mirror/codemirror-darcula.css").toExternalForm());
178     }
179     else {
180       template = template.replace("${css_oldcodemirror}", getClass().getResource("/code-mirror/codemirror-old.css").toExternalForm());
181       template = template.replace("${css_codemirror}", getClass().getResource("/code-mirror/codemirror.css").toExternalForm());
182     }
183     template = template.replace("${code}", content);
184
185     return template;
186   }
187
188   private void updateLookWithProgressBarIfNeeded() {
189     if (myShowProgress) {
190       myProgressBar.setVisible(true);
191       myWebComponent.setVisible(false);
192     }
193   }
194
195   private void initHyperlinkListener() {
196     myEngine.getLoadWorker().stateProperty().addListener((ov, oldState, newState) -> {
197       if (newState == Worker.State.SUCCEEDED) {
198         final EventListener listener = makeHyperLinkListener();
199
200         addListenerToAllHyperlinkItems(listener);
201       }
202     });
203   }
204
205   private void addListenerToAllHyperlinkItems(EventListener listener) {
206     final Document doc = myEngine.getDocument();
207     if (doc != null) {
208       final NodeList nodeList = doc.getElementsByTagName("a");
209       for (int i = 0; i < nodeList.getLength(); i++) {
210         ((EventTarget)nodeList.item(i)).addEventListener(EVENT_TYPE_CLICK, listener, false);
211       }
212     }
213   }
214
215   @NotNull
216   private EventListener makeHyperLinkListener() {
217     return new EventListener() {
218       @Override
219       public void handleEvent(Event ev) {
220         String domEventType = ev.getType();
221         if (domEventType.equals(EVENT_TYPE_CLICK)) {
222           myEngine.setJavaScriptEnabled(true);
223           myEngine.getLoadWorker().cancel();
224           ev.preventDefault();
225           final String href = getLink((Element)ev.getTarget());
226           if (href == null) return;
227           BrowserUtil.browse(href);
228           
229         }
230       }
231
232       @Nullable
233       private String getLink(@NotNull Element element) {
234         final String href = element.getAttribute("href");
235         return href == null ? getLinkFromNodeWithCodeTag(element) : href;
236       }
237
238       @Nullable
239       private String getLinkFromNodeWithCodeTag(@NotNull Element element) {
240         Node parentNode = element.getParentNode();
241         NamedNodeMap attributes = parentNode.getAttributes();
242         while (attributes.getLength() > 0 && attributes.getNamedItem("class") != null) {
243           parentNode = parentNode.getParentNode();
244           attributes = parentNode.getAttributes();
245         }
246         return attributes.getNamedItem("href").getNodeValue();
247       }
248     };
249   }
250
251   public void addBackAndOpenButtons() {
252     ApplicationManager.getApplication().invokeLater(() -> {
253       final JPanel panel = new JPanel();
254       panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
255
256       final JButton backButton = makeGoButton("Click to go back", AllIcons.Actions.Back, -1);
257       final JButton forwardButton = makeGoButton("Click to go forward", AllIcons.Actions.Forward, 1);
258       final JButton openInBrowser = new JButton(AllIcons.Actions.Browser_externalJavaDoc);
259       openInBrowser.addActionListener(e -> BrowserUtil.browse(myEngine.getLocation()));
260       openInBrowser.setToolTipText("Click to open link in browser");
261       addButtonsAvailabilityListeners(backButton, forwardButton);
262
263       panel.setMaximumSize(new Dimension(40, getPanel().getHeight()));
264       panel.add(backButton);
265       panel.add(forwardButton);
266       panel.add(openInBrowser);
267
268       add(panel, BorderLayout.PAGE_START);
269     });
270   }
271
272   private void addButtonsAvailabilityListeners(JButton backButton, JButton forwardButton) {
273     Platform.runLater(() -> myEngine.getLoadWorker().stateProperty().addListener((ov, oldState, newState) -> {
274       if (newState == Worker.State.SUCCEEDED) {
275         final WebHistory history = myEngine.getHistory();
276         boolean isGoBackAvailable = history.getCurrentIndex() > 0;
277         boolean isGoForwardAvailable = history.getCurrentIndex() < history.getEntries().size() - 1;
278         ApplicationManager.getApplication().invokeLater(() -> {
279           backButton.setEnabled(isGoBackAvailable);
280           forwardButton.setEnabled(isGoForwardAvailable);
281         });
282       }
283     }));
284   }
285
286   private JButton makeGoButton(@NotNull final String toolTipText, @NotNull final Icon icon, final int direction) {
287     final JButton button = new JButton(icon);
288     button.setEnabled(false);
289     button.addMouseListener(new MouseAdapter() {
290       @Override
291       public void mouseClicked(MouseEvent e) {
292         if (e.getClickCount() == 1) {
293           Platform.runLater(() -> myEngine.getHistory().go(direction));
294         }
295       }
296     });
297     button.setToolTipText(toolTipText);
298     return button;
299   }
300
301
302   private ProgressBar makeProgressBarWithListener() {
303     final ProgressBar progress = new ProgressBar();
304     progress.progressProperty().bind(myWebComponent.getEngine().getLoadWorker().progressProperty());
305
306     myWebComponent.getEngine().getLoadWorker().stateProperty().addListener(
307       (ov, oldState, newState) -> {
308         if (myWebComponent.getEngine().getLocation().contains("http") && newState == Worker.State.SUCCEEDED) {
309           myProgressBar.setVisible(false);
310           myWebComponent.setVisible(true);
311         }
312       });
313
314     return progress;
315   }
316
317   public JFXPanel getPanel() {
318     return myPanel;
319   }
320
321   private void setPanel(JFXPanel panel) {
322     myPanel = panel;
323   }
324
325   private class StudyLafManagerListener implements LafManagerListener {
326     @Override
327     public void lookAndFeelChanged(LafManager manager) {
328       updateLaf(manager.getCurrentLookAndFeel() instanceof DarculaLookAndFeelInfo);
329     }
330   }
331 }