/*
* Copyright (c) 2015-2015 Vladimir Schneider <vladimir.schneider@gmail.com>
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* This file is based on the IntelliJ SimplePlugin tutorial
*
*/
package com.vladsch.idea.multimarkdown.editor;
import com.intellij.codeHighlighting.BackgroundEditorHighlighter;
import com.intellij.ide.structureView.StructureViewBuilder;
import com.intellij.lang.Language;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.UndoConfirmationPolicy;
import com.intellij.openapi.editor.CaretModel;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.fileEditor.*;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.openapi.vfs.VirtualFile;
import com.vladsch.idea.multimarkdown.MultiMarkdownBundle;
import com.vladsch.idea.multimarkdown.MultiMarkdownPlugin;
import com.vladsch.idea.multimarkdown.MultiMarkdownProjectComponent;
import com.vladsch.idea.multimarkdown.parser.MultiMarkdownLexParserManager;
import com.vladsch.idea.multimarkdown.settings.MultiMarkdownGlobalSettings;
import com.vladsch.idea.multimarkdown.settings.MultiMarkdownGlobalSettingsListener;
import com.vladsch.idea.multimarkdown.util.GitHubLinkResolver;
import com.vladsch.idea.multimarkdown.util.ReferenceChangeListener;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.web.PopupFeatures;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.util.Callback;
import netscape.javascript.JSException;
import netscape.javascript.JSObject;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.pegdown.LinkRenderer;
import org.pegdown.PegDownProcessor;
import org.pegdown.ToHtmlSerializer;
import org.pegdown.ast.RootNode;
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.beans.PropertyChangeListener;
import java.util.Timer;
import java.util.TimerTask;
import static com.vladsch.idea.multimarkdown.editor.MultiMarkdownPathResolver.isWikiDocument;
import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;
//import com.sun.javafx.scene.layout.region.CornerRadiiConverter;
public class MultiMarkdownFxPreviewEditor extends UserDataHolderBase implements FileEditor, Disposable {
protected static final org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(MultiMarkdownFxPreviewEditor.class);
public static final String PREVIEW_EDITOR_NAME = MultiMarkdownBundle.message("multimarkdown.preview-tab-name");
public static final String TEXT_EDITOR_NAME = MultiMarkdownBundle.message("multimarkdown.html-tab-name");
protected static int instances = 0;
/**
* The {@link Component} used to render the HTML preview.
*/
protected final JPanel jEditorPane;
protected WebView webView;
protected WebEngine webEngine;
protected JFXPanel jfxPanel;
protected String scrollOffset = null;
protected AnchorPane anchorPane;
/**
* The {@link Document} previewed in this editor.
*/
protected final Document document;
protected final boolean isWikiDocument;
//protected final EditorTextField myTextViewer;
protected final EditorImpl myTextViewer;
protected boolean isReleased = false;
protected MultiMarkdownGlobalSettingsListener globalSettingsListener;
protected ReferenceChangeListener projectFileListener;
/**
* The {@link PegDownProcessor} used for building the document AST.
*/
//private ThreadLocal<PegDownProcessor> processor = initProcessor();
private PegDownProcessor processor = null;
protected boolean isActive = false;
protected boolean isRawHtml = false;
protected boolean isEditorTabVisible = true;
protected Project project;
protected LinkRenderer linkRendererNormal;
protected LinkRenderer linkRendererModified;
protected String pageScript = null;
protected boolean needStyleSheetUpdate;
protected boolean htmlWorkerRunning;
protected String fireBugJS;
private final VirtualFile containingFile;
private GitHubLinkResolver resolver;
public static boolean isShowModified() {
return MultiMarkdownGlobalSettings.getInstance().showHtmlTextAsModified.getValue();
}
public static int getParsingTimeout() {
return MultiMarkdownGlobalSettings.getInstance().parsingTimeout.getValue();
}
public static int getUpdateDelay() {
return MultiMarkdownGlobalSettings.getInstance().updateDelay.getValue();
}
public static boolean isTaskLists() {
return MultiMarkdownGlobalSettings.getInstance().taskLists.getValue();
}
public static boolean isDarkTheme() {
return MultiMarkdownGlobalSettings.getInstance().isDarkUITheme();
}
public static String getCustomCss() {
return MultiMarkdownGlobalSettings.getInstance().customFxCss.getValue();
}
public static boolean isShowHtmlText() {
return MultiMarkdownGlobalSettings.getInstance().showHtmlText.getValue();
}
/**
* Indicates whether the HTML preview is obsolete and should regenerated from the Markdown {@link #document}.
*/
protected boolean previewIsObsolete = true;
protected Timer updateDelayTimer;
protected final int instance = ++instances;
protected void updateEditorTabIsVisible() {
if (isRawHtml) {
isEditorTabVisible = isShowHtmlText();
getComponent().setVisible(isEditorTabVisible);
} else {
isEditorTabVisible = true;
}
}
protected void checkNotifyUser() {
//final Project project = this.project;
//final MultiMarkdownGlobalSettings settings = MultiMarkdownGlobalSettings.getInstance();
//
//settings.startSuspendNotifications();
//if (settings.isDarkUITheme() && (settings.iconBullets.getValue() || settings.iconTasks.getValue()) && true && !settings.wasShownDarkBug.getValue()) {
// // notify the user that the Icons for Tasks and Bullets will be turned off due to a rendering bug
// settings.wasShownDarkBug.setValue(true);
// NotificationGroup issueNotificationGroup = new NotificationGroup(MultiMarkdownGlobalSettings.NOTIFICATION_GROUP_ISSUES,
// NotificationDisplayType.BALLOON, true, null);
//
// Notification notification = issueNotificationGroup.createNotification("<strong>MultiMarkdown</strong> Plugin Notification",
// "<p>An issue with rendering icons when the UI theme is <strong>Darcula</strong> prevents bullet "+
// "and task list items from using these options. " +
// "These settings will be ignored while <strong>Darcula</strong> "+
// "theme is in effect and until the issue is fixed.</p>\n" +
// "<p> </p>\n" +
// "<p>Feel free leave the <em>Bullets with Icons</em> and <em>Tasks with Icons</em> options turned on. "+
// "They will take effect when they no longer adversely affect the display.</p>\n" +
// "",
// NotificationType.INFORMATION, null);
// notification.setImportant(true);
// Notifications.Bus.notify(notification, project);
//}
//settings.endSuspendNotifications();
}
protected void updateLinkRenderer() {
int options = 0;
if (MultiMarkdownGlobalSettings.getInstance().githubWikiLinks.getValue()) options |= MultiMarkdownLinkRenderer.GITHUB_WIKI_LINK_FORMAT;
linkRendererModified = new MultiMarkdownLinkRenderer(project, document, "absent", null, options | MultiMarkdownLinkRenderer.VALIDATE_LINKS);
linkRendererNormal = new MultiMarkdownLinkRenderer(options);
}
/**
* Build a new instance of {@link MultiMarkdownFxPreviewEditor}.
*
* @param project the {@link Project} containing the document
* @param doc the {@link Document} previewed in this editor.
*/
public MultiMarkdownFxPreviewEditor(@NotNull final Project project, @NotNull Document doc, boolean isRawHtml) {
this.isRawHtml = isRawHtml;
this.document = doc;
this.project = project;
this.isWikiDocument = isWikiDocument(document);
containingFile = FileDocumentManager.getInstance().getFile(document);
resolver = containingFile == null ? null : new GitHubLinkResolver(containingFile, project);
// Listen to the document modifications.
this.document.addDocumentListener(new DocumentAdapter() {
@Override
public void documentChanged(DocumentEvent e) {
delayedHtmlPreviewUpdate(false);
}
});
// Listen to settings changes
MultiMarkdownGlobalSettings.getInstance().addListener(globalSettingsListener = new MultiMarkdownGlobalSettingsListener() {
public void handleSettingsChanged(@NotNull final MultiMarkdownGlobalSettings newSettings) {
if (project.isDisposed()) return;
processor = null;
updateEditorTabIsVisible();
updateLinkRenderer();
delayedHtmlPreviewUpdate(true);
checkNotifyUser();
}
});
MultiMarkdownProjectComponent projectComponent = MultiMarkdownPlugin.getProjectComponent(project);
if (projectComponent != null) {
projectComponent.addListener(projectFileListener = new ReferenceChangeListener() {
@Override
public void referenceChanged(@Nullable String name) {
if (project.isDisposed()) return;
delayedHtmlPreviewUpdate(false);
}
});
}
project.getMessageBus().connect(this).subscribe(DumbService.DUMB_MODE, new DumbService.DumbModeListener() {
@Override
public void enteredDumbMode() {
}
@Override
public void exitDumbMode() {
// need to re-evaluate class link accessibility
if (project.isDisposed()) return;
delayedHtmlPreviewUpdate(false);
}
});
updateLinkRenderer();
if (isRawHtml) {
jEditorPane = null;
jfxPanel = null;
webView = null;
webEngine = null;
Language language = Language.findLanguageByID("HTML");
FileType fileType = language != null ? language.getAssociatedFileType() : null;
Document myDocument = EditorFactory.getInstance().createDocument("");
myTextViewer = (EditorImpl) EditorFactory.getInstance().createViewer(myDocument, project);
if (fileType != null)
myTextViewer.setHighlighter(EditorHighlighterFactory.getInstance().createEditorHighlighter(project, fileType));
} else {
// Setup the editor pane for rendering HTML.
myTextViewer = null;
jEditorPane = new JPanel(new BorderLayout(), false);
jfxPanel = new JFXPanel(); // initializing javafx
jEditorPane.add(jfxPanel, BorderLayout.CENTER);
Platform.setImplicitExit(false);
Platform.runLater(new Runnable() {
@Override
public void run() {
if (project.isDisposed()) return;
webView = new WebView();
webEngine = webView.getEngine();
anchorPane = new AnchorPane();
AnchorPane.setTopAnchor(webView, 0.0);
AnchorPane.setLeftAnchor(webView, 0.0);
AnchorPane.setBottomAnchor(webView, 0.0);
AnchorPane.setRightAnchor(webView, 0.0);
anchorPane.getChildren().add(webView);
jfxPanel.setScene(new Scene(anchorPane));
webEngine.setCreatePopupHandler(new Callback<PopupFeatures, WebEngine>() {
@Override
public WebEngine call(PopupFeatures config) {
// return a web engine for the new browser window or null to block popups
return null;
}
});
addStateChangeListener();
}
});
}
checkNotifyUser();
}
protected void addStateChangeListener() {
webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() {
@Override
public void changed(ObservableValue<? extends Worker.State> observable, Worker.State oldState, Worker.State newState) {
if (project.isDisposed()) return;
workerStateChanged(observable, oldState, newState);
}
});
}
protected void workerStateChanged(ObservableValue<? extends Worker.State> observable, Worker.State oldState, Worker.State newState) {
//logger.info("[" + instance + "] " + "newState: " + newState + ", oldState: " + oldState);
if (newState == Worker.State.SUCCEEDED) {
// restore scroll if we had it
JSObject jsobj = (JSObject) webEngine.executeScript("window");
jsobj.setMember("java", new JSBridge(this));
EventListener listener = new EventListener() {
@Override
public void handleEvent(org.w3c.dom.events.Event evt) {
evt.stopPropagation();
evt.preventDefault();
if (project.isDisposed()) return;
Element link = (Element) evt.getCurrentTarget();
org.w3c.dom.Document doc = webEngine.getDocument();
final String href = link.getAttribute("href");
if (href.charAt(0) == '#') {
if (href.length() != 1) {
// tries to go to an anchor
String hrefName = href.substring(1);
// scroll it into view
try {
JSObject result = (JSObject) webEngine.executeScript("(function () {\n" +
" var elemTop = 0;\n" +
" var elems = '';\n" +
" var elem = window.document.getElementById('" + hrefName + "');\n" +
" if (!elem) {\n" +
" var elemList = window.document.getElementsByTagName('a');\n" +
" for (a in elemList) {\n" +
" var aElem = elemList[a]\n" +
" if (aElem.hasOwnProperty('name') && aElem.name == '" + hrefName + "') {\n" +
" elem = aElem;\n" +
" break;\n" +
" }\n" +
" }\n" +
" }\n" +
" if (elem) {\n" +
" while (elem && elem.tagName !== 'HTML') {\n" +
" elems += ',' + elem.tagName + ':' + elem.offsetTop\n" +
" if (elem.offsetTop) {\n" +
" elemTop += elem.offsetTop;\n" +
" break;\n" +
" }\n" +
" elem = elem.parentNode\n" +
" }\n" +
" }\n" +
" return { elemTop: elemTop, elems: elems, found: !!elem };\n" +
"})()" +
"");
int elemTop = (Integer) result.getMember("elemTop");
boolean elemFound = (Boolean) result.getMember("found");
String parentList = (String) result.getMember("elems");
//logger.trace(parentList);
if (elemFound) webEngine.executeScript("window.scroll(0, " + elemTop + ")");
} catch (JSException ex) {
String error = ex.toString();
logger.info("[" + instance + "] " + "JSException on script", ex);
}
}
} else {
MultiMarkdownPathResolver.launchExternalLink(project, href);
}
}
};
NodeList nodeList;
org.w3c.dom.Document doc = webEngine.getDocument();
if (doc != null) {
((EventTarget) doc.getDocumentElement()).addEventListener("contextmenu", new EventListener() {
@Override
public void handleEvent(Event evt) {
evt.preventDefault();
}
}, false);
Element el = doc.getElementById("a");
nodeList = doc.getElementsByTagName("a");
for (int i = 0; i < nodeList.getLength(); i++) {
((EventTarget) nodeList.item(i)).addEventListener("click", listener, false);
}
// all images are mapped during conversion. Any relative ones are not resolved.
//nodeList = doc.getElementsByTagName("img");
//for (int i = 0; i < nodeList.getLength(); i++) {
// HTMLImageElementImpl imgNode = (HTMLImageElementImpl) nodeList.item(i);
// String src = imgNode.getSrc();
// if (!src.startsWith("http://") && !src.startsWith("https://") && !src.startsWith("ftp://") && !src.startsWith("file://")) {
// // relative to document, change it to absolute file://
// // this means it does not resolve, leave it
// if (!project.isDisposed() && containingFile != null && resolver != null) {
// ImageLinkRef linkRef = new ImageLinkRef(new FileRef(containingFile), src, null, null);
// PathInfo resolvedTarget = resolver.resolve(linkRef, LinkResolver.ONLY_URI, null);
//
// assert resolvedTarget == null || resolvedTarget instanceof LinkRef && linkRef.isURI() : "Expected URI LinkRef, got " + linkRef;
// if (resolvedTarget != null) {
// imgNode.setSrc(resolvedTarget.getFilePath());
// }
// }
// }
//}
}
if (pageScript != null && pageScript.length() > 0) {
webEngine.executeScript(pageScript);
}
// enable debug if it is enabled in settings
if (MultiMarkdownGlobalSettings.getInstance().enableFirebug.getValue()) {
enableDebug();
}
String scroll = scrollOffset;
if (scroll != null) {
try {
//webEngine.executeScript("window.java.log('test info')");
webEngine.executeScript("" +
"window.setTimeout(function () { " +
" window.java.log('before scroll');" +
" " + scroll + ";\n" +
" window.java.log('after scroll');" +
"}, 50);");
} catch (Exception e) {
logger.info("[" + instance + "] " + "JSException on script", e);
}
}
try {
webEngine.executeScript("window.addEventListener('scroll', function() { " +
" window.java.onScroll();" +
" window.setTimeout(function () { " +
" window.java.repaint()" +
" }, 100);" +
"})");
} catch (Exception e) {
logger.info("[" + instance + "] " + "", e);
}
//if (needStyleSheetUpdate) {
// setStyleSheet();
//}
}
if (newState == Worker.State.READY || newState == Worker.State.FAILED || newState == Worker.State.SUCCEEDED) {
htmlWorkerRunning = false;
}
}
// call backs from JavaScript will be handled by the bridge
public static class JSBridge {
final MultiMarkdownFxPreviewEditor editor;
public JSBridge(MultiMarkdownFxPreviewEditor editor) {
this.editor = editor;
}
public void log(String msg) {
//logger.info("[" + editor.instance + "] " + msg);
}
public void repaint() {
//logger.info("[" + editor.instance + "] " + "before repaint");
//jEditorPane.invalidate();
if (editor.project.isDisposed()) return;
editor.jfxPanel.repaint();
//logger.info("[" + editor.instance + "] " + "after repaint");
}
public void onScroll() {
if (editor.project.isDisposed()) return;
JSObject scrollPos = (JSObject) editor.webEngine.executeScript("({ x: window.pageXOffset, y: window.pageYOffset })");
//logger.info("[" + editor.instance + "] " + "window scrolled to " + scrollPos.getMember("x") + ", " + scrollPos.getMember("y"));
editor.scrollOffset = "window.scroll(" + scrollPos.getMember("x") + ", " + scrollPos.getMember("y") + ")";
}
}
protected String makeHtmlPage(String html) {
VirtualFile file = FileDocumentManager.getInstance().getFile(document);
String result = "<head>\n" +
"";
final MultiMarkdownGlobalSettings globalSettings = MultiMarkdownGlobalSettings.getInstance();
// load colors css
if (!(globalSettings.useCustomCss(true) && globalSettings.includesColorsCss.getValue())) {
result += "" +
"<link rel=\"stylesheet\" href=\"" + globalSettings.getColorsCssExternalForm(true) + "\">\n" +
"";
}
// load layout css
if (!(globalSettings.useCustomCss(true) && globalSettings.includesLayoutCss.getValue())) {
result += "" +
"<link rel=\"stylesheet\" href=\"" + globalSettings.getLayoutCssExternalForm(true) + "\">\n" +
"";
}
// load highlight & css
if (globalSettings.useHighlightJs.getValue()) {
if (!(globalSettings.useCustomCss(true) && globalSettings.includesHljsCss.getValue())) {
result += "" +
"<link rel=\"stylesheet\" href=\"" + globalSettings.getHljsCssExternalForm(true) + "\">\n" +
"";
}
}
// load custom css
if (globalSettings.useCustomCss(true)) {
result += "" +
"<link rel=\"stylesheet\" href=\"" + globalSettings.getCustomCssExternalForm(true) + "\">\n" +
"";
}
// load highlight js script
if (globalSettings.useHighlightJs.getValue()) {
result += "" +
"<script src=\"" + globalSettings.getHighlighJsExternalForm(true) + "\"></script>\n" +
"";
}
result += "" +
"<title>" + escapeHtml(file != null ? file.getName() : "<null>") + "</title>\n" +
"</head>\n" +
"<body>\n" +
"";
String gitHubHref = MultiMarkdownPathResolver.getGitHubDocumentURL(project, document, !isWikiDocument);
String gitHubClose = "";
if (isWikiDocument) {
if (gitHubHref == null) {
gitHubHref = "";
} else {
gitHubHref = "<a href=\"" + gitHubHref + "\" name=\"wikipage\" id=\"wikipage\" class=\"anchor\"><span class=\"octicon octicon-link\"></span>";
gitHubClose = "</a>";
}
result += "" +
"<div class=\"wiki-container\">\n" +
"<h1>" + gitHubHref + gitHubClose + escapeHtml(file != null ? file.getNameWithoutExtension().replace('-', ' ') : "<null>") + "</h1>\n" +
"<article class=\"wiki-body\">\n" +
"";
} else {
if (gitHubHref == null) {
gitHubHref = "";
} else {
gitHubHref = "<a href=\"" + gitHubHref + "\" name=\"markdown-page\" id=\"markdown-page\" class=\"page-anchor\">";
gitHubClose = "</a>";
}
result += "" +
"<div class=\"container\">\n" +
"<div id=\"readme\" class=\"boxed-group\">\n" +
"<h3>\n" +
" " + gitHubHref + "<span class=\"bookicon octicon-book\"></span>\n" + gitHubClose +
" " + (file != null ? file.getName() : "<null>") + "\n" +
"</h3>\n" +
"<article class=\"markdown-body\">\n" +
"";
}
result += html;
result += "\n</article>\n";
result += "</div>\n";
result += "</div>\n";
result += "" +
"<script>hljs.initHighlightingOnLoad();</script>\n" +
"</body>\n";
return result;
}
public void enableDebug() {
try {
webEngine.executeScript("if (!document.getElementById('FirebugLite')) {\n" +
" E = document['createElement' + 'NS'] && document.documentElement.namespaceURI;\n" +
" E = E ? document['createElement' + 'NS'](E, 'script') : document['createElement']('script');\n" +
" E['setAttribute']('id', 'FirebugLite');\n" +
" E['setAttribute']('src', 'https://getfirebug.com/' + 'firebug-lite.js' + '#startOpened');\n" +
//" E['setAttribute']('src', '" + fireBugJS + "');\n" +
" E['setAttribute']('FirebugLite', '4');\n" +
" (document['getElementsByTagName']('head')[0] || document['getElementsByTagName']('body')[0]).appendChild(E);\n" +
" E = new Image;\n" +
" E['setAttribute']('src', 'https://getfirebug.com/' + '#startOpened');\n" +
"}\n");
} catch (JSException ex) {
String error = ex.toString();
logger.info("[" + instance + "] " + "JSException on script", ex);
}
}
protected void delayedHtmlPreviewUpdate(final boolean fullKit) {
if (updateDelayTimer != null) {
updateDelayTimer.cancel();
updateDelayTimer = null;
}
if (project.isDisposed()) return;
if (!isEditorTabVisible)
return;
updateDelayTimer = new Timer();
updateDelayTimer.schedule(new TimerTask() {
@Override
public void run() {
if (project.isDisposed()) return;
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
if (project.isDisposed()) return;
previewIsObsolete = true;
if (fullKit) {
needStyleSheetUpdate = true;
//processor.remove(); // make it re-initialize when accessed
}
updateHtmlContent(isActive || isMyTabSelected());
}
}, ModalityState.any());
}
}, getUpdateDelay());
}
protected boolean isMyTabSelected() {
FileEditorManager manager = FileEditorManager.getInstance(project);
FileEditor[] editors = manager.getSelectedEditors();
for (FileEditor editor : editors) {
if (editor == this) return true;
}
return false;
}
protected MultiMarkdownFxPreviewEditor findCounterpart() {
// here we can find our HTML Text counterpart and update its HTML at the same time. but it is better to keep it separate for now
VirtualFile file = FileDocumentManager.getInstance().getFile(document);
if (file != null) {
FileEditorManager manager = FileEditorManager.getInstance(project);
FileEditor[] editors = manager.getEditors(file);
for (FileEditor editor : editors) {
if (editor != this && editor instanceof MultiMarkdownFxPreviewEditor) {
return (MultiMarkdownFxPreviewEditor) editor;
}
}
}
return null;
}
//protected void setStyleSheet() {
// if (isRawHtml) return;
//
// needStyleSheetUpdate = false;
// webEngine.setUserStyleSheetLocation(MultiMarkdownGlobalSettings.getInstance().getCssExternalForm());
//}
protected void updateRawHtmlText(final String htmlTxt) {
final DocumentEx myDocument = myTextViewer.getDocument();
if (project.isDisposed()) return;
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
if (project.isDisposed()) return;
CommandProcessor.getInstance().executeCommand(project, new Runnable() {
@Override
public void run() {
if (project.isDisposed()) return;
myDocument.replaceString(0, myDocument.getTextLength(), htmlTxt);
final CaretModel caretModel = myTextViewer.getCaretModel();
if (caretModel.getOffset() >= myDocument.getTextLength()) {
caretModel.moveToOffset(myDocument.getTextLength());
}
}
}, null, null, UndoConfirmationPolicy.DEFAULT, myDocument);
}
});
}
protected String markdownToHtml(boolean modified, RootNode rootNode) {
if (rootNode == null) {
return "<strong>Parser timed out</strong>";
} else {
if (modified) {
MultiMarkdownToHtmlSerializer htmlSerializer = new MultiMarkdownToHtmlSerializer(project, document, linkRendererModified);
if (!isWikiDocument) {
htmlSerializer.setFlag(MultiMarkdownToHtmlSerializer.NO_WIKI_LINKS);
}
return htmlSerializer.toHtml(rootNode).replace("<br/>", "<br/>\n");
} else {
return new ToHtmlSerializer(linkRendererNormal).toHtml(rootNode).replace("<br/>", "<br/>\n");
}
}
}
protected void updateHtmlContent(boolean force) {
if (updateDelayTimer != null) {
updateDelayTimer.cancel();
updateDelayTimer = null;
}
if (previewIsObsolete && isEditorTabVisible && (isActive || force)) {
try {
final RootNode rootNode = MultiMarkdownLexParserManager.parseMarkdownRoot(document.getCharsSequence(), MultiMarkdownGlobalSettings.getInstance().getExtensionsValue(), getParsingTimeout());
if (isRawHtml) {
final String htmlTxt = isShowModified() ? makeHtmlPage(markdownToHtml(true, rootNode)) : markdownToHtml(false, rootNode);
updateRawHtmlText(htmlTxt);
} else {
if (!htmlWorkerRunning) {
htmlWorkerRunning = true;
previewIsObsolete = false;
final String html = makeHtmlPage(markdownToHtml(true, rootNode));
Platform.runLater(new Runnable() {
@Override
public void run() {
if (project.isDisposed()) return;
// TODO: add option to enable/disable keeping scroll position on update
Worker.State state = webEngine.getLoadWorker().getState();
//logger.info("[" + instance + "] " + "State on update " + state);
Double pageZoom = MultiMarkdownGlobalSettings.getInstance().pageZoom.getValue();
if (webView.getZoom() != pageZoom) {
//logger.info("[" + instance + "] " + "setZoom(" + pageZoom + ")");
webView.setZoom(pageZoom);
}
//logger.info("[" + instance + "] " + "loadContent");
webEngine.loadContent(html);
}
});
} else {
// reschedule the update for later
delayedHtmlPreviewUpdate(false);
}
}
} catch (Exception e) {
logger.info("[" + instance + "] " + "Failed processing Markdown document", e);
}
}
}
@NotNull
public JComponent getComponent() {
//return scrollPane != null ? scrollPane : myTextViewer.getComponent();
return jEditorPane != null ? jEditorPane : myTextViewer.getComponent();
}
@Nullable
public JComponent getPreferredFocusedComponent() {
//return scrollPane != null ? scrollPane : myTextViewer.getComponent();
return jEditorPane != null ? jEditorPane : myTextViewer.getContentComponent();
}
@NotNull
@NonNls
public String getName() {
return isRawHtml ? TEXT_EDITOR_NAME : PREVIEW_EDITOR_NAME;
}
/**
* Get the state of the editor.
* <p/>
* Just returns {@link FileEditorState#INSTANCE} as {@link MultiMarkdownFxPreviewEditor} is stateless.
*
* @param level the level.
* @return {@link FileEditorState#INSTANCE}
* @see #setState(FileEditorState)
*/
@NotNull
public FileEditorState getState(@NotNull FileEditorStateLevel level) {
return FileEditorState.INSTANCE;
}
/**
* Set the state of the editor.
* <p/>
* Does not do anything as {@link MultiMarkdownFxPreviewEditor} is stateless.
*
* @param state the new state.
* @see #getState(FileEditorStateLevel)
*/
public void setState(@NotNull FileEditorState state) {
}
/**
* Indicates whether the document content is modified compared to its file.
*
* @return {@code false} as {@link MultiMarkdownFxPreviewEditor} is read-only.
*/
public boolean isModified() {
return false;
}
/**
* Indicates whether the editor is valid.
*
* @return {@code true} if {@link #document} content is readable.
*/
public boolean isValid() {
return true;
}
/**
* Invoked when the editor is selected.
* <p/>
* Update the HTML content if obsolete.
*/
public void selectNotify() {
isActive = true;
if (previewIsObsolete) {
updateHtmlContent(false);
}
}
/**
* Invoked when the editor is deselected.
* <p/>
* Does nothing.
*/
public void deselectNotify() {
isActive = false;
}
/**
* Add specified listener.
* <p/>
* Does nothing.
*
* @param listener the listener.
*/
public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) {
}
/**
* Remove specified listener.
* <p/>
* Does nothing.
*
* @param listener the listener.
*/
public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) {
}
/**
* Get the background editor highlighter.
*
* @return {@code null} as {@link MultiMarkdownFxPreviewEditor} does not require highlighting.
*/
@Nullable
public BackgroundEditorHighlighter getBackgroundHighlighter() {
return null;
}
/**
* Get the current location.
*
* @return {@code null} as {@link MultiMarkdownFxPreviewEditor} is not navigable.
*/
@Nullable
public FileEditorLocation getCurrentLocation() {
return null;
}
/**
* Get the structure view builder.
*
* @return TODO {@code null} as parsing/PSI is not implemented.
*/
@Nullable
public StructureViewBuilder getStructureViewBuilder() {
return null;
}
/**
* Dispose the editor.
*/
public void dispose() {
if (!isReleased) {
isReleased = true;
if (updateDelayTimer != null) {
updateDelayTimer.cancel();
updateDelayTimer = null;
}
if (jEditorPane != null) {
jEditorPane.removeAll();
}
if (globalSettingsListener != null) {
MultiMarkdownGlobalSettings.getInstance().removeListener(globalSettingsListener);
globalSettingsListener = null;
}
if (myTextViewer != null) {
final Application application = ApplicationManager.getApplication();
final Runnable runnable = new Runnable() {
@Override
public void run() {
if (!myTextViewer.isDisposed()) {
EditorFactory.getInstance().releaseEditor(myTextViewer);
}
}
};
if (application.isUnitTestMode() || application.isDispatchThread()) {
runnable.run();
} else {
application.invokeLater(runnable);
}
}
Disposer.dispose(this);
}
}
}