package com.kodcu.component;
import com.kodcu.config.EditorConfigBean;
import com.kodcu.config.FoldStyle;
import com.kodcu.config.SpellcheckConfigBean;
import com.kodcu.controller.ApplicationController;
import com.kodcu.keyboard.KeyHelper;
import com.kodcu.other.IOHelper;
import com.kodcu.service.DirectoryService;
import com.kodcu.service.ParserService;
import com.kodcu.service.ThreadService;
import com.kodcu.service.convert.markdown.MarkdownService;
import com.kodcu.service.extension.AsciiTreeGenerator;
import com.kodcu.service.shortcut.ShortcutProvider;
import com.kodcu.service.ui.TabService;
import com.kodcu.spell.dictionary.Token;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.*;
import javafx.geometry.Bounds;
import javafx.scene.control.*;
import javafx.scene.input.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.util.Callback;
import netscape.javascript.JSObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Created by usta on 09.04.2015.
*/
@Component
@Scope("prototype")
public class EditorPane extends AnchorPane {
private final WebView webView;
private final Logger logger = LoggerFactory.getLogger(EditorPane.class);
private final ApplicationController controller;
private final EditorConfigBean editorConfigBean;
private final ThreadService threadService;
private final ShortcutProvider shortcutProvider;
private final ApplicationContext applicationContext;
private final TabService tabService;
private final AsciiTreeGenerator asciiTreeGenerator;
private final ParserService parserService;
private final SpellcheckConfigBean spellcheckConfigBean;
private final ObservableList<Runnable> handleReadyTasks;
private String mode = "ace/mode/asciidoc";
private String initialEditorValue = "";
private Path path;
private FileTime lastModifiedTime;
private static String lastInterPath;
private final String escapeBackSlash = "(?<!\\\\)"; // ignores if word started with \
private final String ignoreSuffix = "(?<!\\\\)"; // ignores if word started with \
private final BooleanProperty ready = new SimpleBooleanProperty(false);
private final ObjectProperty<Path> spellLanguage = new SimpleObjectProperty<>();
private final AtomicBoolean contextOpen = new AtomicBoolean(false);
private final BooleanProperty changedProperty = new SimpleBooleanProperty(false);
@Value("${application.live.url}")
private String liveUrl;
@Value("${application.preview.url}")
private String previewUrl;
private final DirectoryService directoryService;
private ContextMenu contextMenu;
private Number pageX;
private Number pageY;
@Autowired
public EditorPane(ApplicationController controller, EditorConfigBean editorConfigBean, ThreadService threadService, ShortcutProvider shortcutProvider, ApplicationContext applicationContext, TabService tabService, AsciiTreeGenerator asciiTreeGenerator, ParserService parserService, SpellcheckConfigBean spellcheckConfigBean, DirectoryService directoryService) {
this.setVisible(false);
this.controller = controller;
this.editorConfigBean = editorConfigBean;
this.threadService = threadService;
this.shortcutProvider = shortcutProvider;
this.applicationContext = applicationContext;
this.tabService = tabService;
this.asciiTreeGenerator = asciiTreeGenerator;
this.spellcheckConfigBean = spellcheckConfigBean;
this.directoryService = directoryService;
this.handleReadyTasks = FXCollections.observableArrayList();
this.parserService = parserService;
this.webView = new WebView();
this.ready.addListener(this::afterEditorReady);
webEngine().setConfirmHandler(this::handleConfirm);
initializeMargins();
initializeEditorContextMenus();
}
private Boolean handleConfirm(String param) {
if ("command:ready".equals(param)) {
afterEditorLoaded();
}
return false;
}
private void afterEditorLoaded() {
getWindow().setMember("afx", controller);
getWindow().setMember("editorPane", this);
updateOptions();
if (Objects.nonNull(path)) {
threadService.runTaskLater(() -> {
final String content = IOHelper.readFile(path);
setLastModifiedTime(IOHelper.getLastModifiedTime(path));
threadService.runActionLater(() -> {
changeEditorMode();
setInitialized();
setEditorValue(content);
resetUndoManager();
ready.setValue(true);
});
});
} else {
setInitialized();
setEditorValue(initialEditorValue);
resetUndoManager();
ready.setValue(true);
}
this.getChildren().add(webView);
webView.requestFocus();
}
private void afterEditorReady(ObservableValue observable, boolean oldValue, boolean newValue) {
if (newValue) {
ObservableList<Runnable> runnables = FXCollections.observableArrayList(handleReadyTasks);
handleReadyTasks.clear();
for (Runnable runnable : runnables) {
runnable.run();
}
updatePreviewUrl();
}
}
public void updatePreviewUrl() {
final String interPath = directoryService.interPath();
final boolean isSameInterPath = Optional.ofNullable(interPath)
.filter(i -> !i.isEmpty())
.filter(i -> i.equals(lastInterPath))
.isPresent();
if (Objects.isNull(lastInterPath)) {
lastInterPath = interPath;
return;
}
if (isSameInterPath) {
this.rerender();
return;
}
threadService.runActionLater(() -> {
if (is("asciidoc") || is("markdown")) {
applicationContext.getBean(HtmlPane.class)
.load(String.format(previewUrl, controller.getPort(), lastInterPath = interPath));
} else if (is("html")) {
applicationContext.getBean(LiveReloadPane.class)
.load(String.format(liveUrl, controller.getPort(), lastInterPath = interPath));
}
}, true);
}
private void updateOptions() {
webEngine().executeScript("updateOptions()");
}
private void setInitialized() {
webEngine().executeScript("setInitialized()");
}
private void resetUndoManager() {
webEngine().executeScript("resetUndoManager()");
}
private void initializeMargins() {
AnchorPane.setBottomAnchor(this, 0D);
AnchorPane.setTopAnchor(this, 0D);
AnchorPane.setLeftAnchor(this, 0D);
AnchorPane.setRightAnchor(this, 0D);
VBox.setVgrow(this, Priority.ALWAYS);
AnchorPane.setBottomAnchor(webView, 0D);
AnchorPane.setTopAnchor(webView, 0D);
AnchorPane.setLeftAnchor(webView, 0D);
AnchorPane.setRightAnchor(webView, 0D);
VBox.setVgrow(webView, Priority.ALWAYS);
}
public void load(String url) {
if (Objects.nonNull(url))
threadService.runActionLater(() -> {
webEngine().load(url);
}, true);
else
logger.error("Url is not loaded. Reason: null reference");
}
public String getLocation() {
return webEngine().getLocation();
}
public Object call(String methodName, Object... args) {
return getWindow().call(methodName, args);
}
public WebEngine webEngine() {
return webView.getEngine();
}
public WebView getWebView() {
return webView;
}
public void confirmHandler(Callback<String, Boolean> confirmHandler) {
webEngine().setConfirmHandler(confirmHandler);
}
public JSObject getWindow() {
return (JSObject) webEngine().executeScript("window");
}
public String getEditorValue() {
return (String) webEngine().executeScript("editor.getValue()");
}
public void setEditorValue(String value) {
threadService.runActionLater(() -> {
getWindow().setMember("editorValue", value);
webEngine().executeScript("setEditorValue(editorValue)");
getWebView().requestFocus();
updateFoldStyle();
});
}
@WebkitCall(from = "editor")
public void onThemeLoaded() {
if (!isVisible()) {
setVisible(true);
getWebView().requestFocus();
}
}
public void switchMode(Object... args) {
threadService.runActionLater(() -> {
this.call("switchMode", args);
});
updateFoldStyle();
}
public void rerender(Object... args) {
try {
webEngine().executeScript("rerender()");
} catch (Exception e) {
// no-op
}
}
public void focus() {
webView.requestFocus();
}
public void moveCursorTo(Integer lineno) {
if (Objects.nonNull(lineno)) {
final Optional<ViewPanel> viewPanelOptional = controller.getRightShowerHider().getShowing();
viewPanelOptional.ifPresent(ViewPanel::disableScrollingAndJumping);
try {
webEngine().executeScript(String.format("editor.gotoLine(%d,3,false)", (lineno)));
webEngine().executeScript(String.format("editor.scrollToLine(%d,false,false,function(){})", (lineno - 1)));
} catch (Exception e) {
logger.error("Error occured while moving cursor to line {}", lineno);
}
viewPanelOptional.ifPresent(ViewPanel::enableScrollingAndJumping);
}
}
public void changeEditorMode() {
if (Objects.nonNull(path)) {
String mode = (String) webEngine().executeScript(String.format("changeEditorMode(\"%s\")", path.toUri().toString()));
setMode(mode);
}
updateFoldStyle();
}
public String editorMode() {
return (String) webEngine().executeScript("editorMode()");
}
public void fillModeList(ObservableList modeList) {
threadService.runActionLater(() -> {
this.call("fillModeList", modeList);
});
}
public boolean is(String mode) {
return ("ace/mode/" + mode).equalsIgnoreCase(this.mode);
}
public void setMode(String mode) {
this.mode = mode;
}
public String getMode() {
return mode;
}
public void setTheme(String theme) {
threadService.runActionLater(() -> {
this.call("changeTheme", theme);
});
}
public void setFontSize(int fontSize) {
threadService.runActionLater(() -> {
this.call("changeFontSize", fontSize);
});
}
public void setShowGutter(Boolean showGutter) {
threadService.runActionLater(() -> {
this.call("setShowGutter", showGutter);
});
}
public void setUseWrapMode(Boolean useWrapMode) {
threadService.runActionLater(() -> {
this.call("setUseWrapMode", useWrapMode);
});
}
public void setWrapLimitRange(Integer wrapLimitRange) {
threadService.runActionLater(() -> {
this.call("setWrapLimitRange", wrapLimitRange);
});
}
public void insert(String text) {
threadService.runActionLater(() -> {
JSObject editor = (JSObject) webEngine().executeScript("editor");
editor.call("insert", text);
});
}
public void execCommand(String command) {
threadService.runActionLater(() -> {
JSObject editor = (JSObject) webEngine().executeScript("editor");
editor.call("execCommand", command);
});
}
public String editorSelection() {
return (String) webEngine().executeScript("editor.session.getTextRange(editor.getSelectionRange())");
}
public void initializeEditorContextMenus() {
webView.setContextMenuEnabled(false);
contextMenu = new ContextMenu();
MenuItem cut = MenuItemBuilt.item("Cut").click(e -> {
controller.cutCopy(editorSelection());
execCommand("cut");
});
MenuItem copy = MenuItemBuilt.item("Copy").click(e -> {
controller.cutCopy(editorSelection());
});
MenuItem paste = MenuItemBuilt.item("Paste").click(e -> {
controller.paste();
});
MenuItem pasteRaw = MenuItemBuilt.item("Paste raw").click(e -> {
controller.pasteRaw();
});
MenuItem indexSelection = MenuItemBuilt.item("Index selection").click(e -> {
shortcutProvider.getProvider().addIndexSelection();
});
MenuItem includeAsSubDocument = MenuItemBuilt.item("Include selection").click(e -> {
shortcutProvider.getProvider().includeAsSubdocument();
});
MenuItem replacements = MenuItemBuilt.item("Apply Replacements").click(this::replaceSubs);
MenuItem markdownToAsciidoc = MenuItemBuilt.item("Markdown to Asciidoc").click(e -> {
MarkdownService markdownService = applicationContext.getBean(MarkdownService.class);
markdownService.convertToAsciidoc(getEditorValue(),
content -> threadService.runActionLater(() -> {
tabService.newDoc(content);
}));
});
final Menu editorLanguage = new Menu("Editor language");
final Menu defaultLanguage = new Menu("Default language");
ToggleGroup editorLanguageGroup = new ToggleGroup();
ToggleGroup defaultLanguageGroup = new ToggleGroup();
final RadioMenuItem disableSpeller = CheckItemBuilt.check("Disable spell check", false)
.bindBi(spellcheckConfigBean.disableSpellCheckProperty())
.click(e -> {
checkSpelling();
})
.build();
Menu languageMenu = new Menu("Spell Checker");
languageMenu.getItems().addAll(editorLanguage, defaultLanguage, disableSpeller);
EventHandler<Event> contextMenuRequested = event -> {
final ObservableList<MenuItem> contextMenuItems = contextMenu.getItems();
final List<MenuItem> menuItems = Arrays.asList(cut, copy, paste, pasteRaw,
markdownToAsciidoc,
replacements,
indexSelection,
includeAsSubDocument,
languageMenu);
for (MenuItem menuItem : menuItems) {
if (!contextMenuItems.contains(menuItem)) {
contextMenuItems.add(menuItem);
}
}
if (editorLanguage.getItems().isEmpty()) {
editorLanguage.getItems().add(CheckItemBuilt.check("Use default language", true)
.click(e -> {
setSpellLanguage(null);
checkSpelling();
})
.group(editorLanguageGroup)
.build());
final ObservableList<Path> languages = spellcheckConfigBean.getLanguages();
for (Path language : languages) {
final String pathCleanName = IOHelper.getPathCleanName(language);
editorLanguage.getItems()
.add(CheckItemBuilt.check(pathCleanName, false)
.click(e -> {
setSpellLanguage(language);
checkSpelling();
})
.group(editorLanguageGroup)
.build());
defaultLanguage.getItems()
.add(CheckItemBuilt.check(pathCleanName, spellcheckConfigBean.defaultLanguageProperty().isEqualTo(language).get())
.click(e -> {
spellcheckConfigBean.setDefaultLanguage(language);
checkSpelling();
})
.group(defaultLanguageGroup)
.build());
}
}
if (contextMenu.isShowing()) {
contextMenu.hide();
}
markdownToAsciidoc.setVisible(isMarkdown());
indexSelection.setVisible(isAsciidoc());
if (event instanceof MouseEvent) {
MouseEvent mouseEvent = (MouseEvent) event;
contextMenu.show(getWebView(), mouseEvent.getSceneX(), mouseEvent.getSceneY() + 20);
contextOpen.set(true);
} else {
updateCursorCoordinates();
Bounds bounds = getWebView().localToScene(getWebView().getLayoutBounds());
contextMenu.show(getWebView(), pageX.doubleValue() + bounds.getMinX(), pageY.doubleValue() + bounds.getMinY() + 35);
contextOpen.set(true);
}
checkWordSuggestions();
};
getWebView().addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (KeyHelper.isContextMenu(event)) {
event.consume();
contextMenuRequested.handle(event);
return;
}
if (contextMenu.isShowing()) {
contextMenu.hide();
}
});
getWebView().addEventFilter(KeyEvent.ANY, event -> {
if (contextOpen.get()) {
if (KeyHelper.isEnter(event)) {
event.consume();
}
}
});
contextMenu.setOnHidden(event -> {
threadService.runActionLater(() -> {
contextOpen.set(false);
}, true);
});
getWebView().setOnMouseClicked(event -> {
if (event.getButton() == MouseButton.SECONDARY) {
event.consume();
contextMenuRequested.handle(event);
} else {
if (contextMenu.isShowing()) {
contextMenu.hide();
}
}
});
getWebView().setOnDragDropped(event -> {
Dragboard dragboard = event.getDragboard();
boolean success = false;
if (dragboard.hasFiles() && !dragboard.hasString()) {
List<File> dragboardFiles = dragboard.getFiles();
if (dragboardFiles.size() == 1) {
Path path = dragboardFiles.get(0).toPath();
if (Files.isDirectory(path)) {
threadService.runTaskLater(() -> {
StringBuffer buffer = new StringBuffer();
buffer.append("[tree,file=\"\"]");
buffer.append("\n--\n");
buffer.append(asciiTreeGenerator.generate(path));
buffer.append("\n--");
threadService.runActionLater(() -> {
insert(buffer.toString());
});
});
success = true;
}
}
Optional<String> block = parserService.toImageBlock(dragboardFiles);
if (block.isPresent()) {
insert(block.get());
success = true;
} else {
block = parserService.toIncludeBlock(dragboardFiles);
if (block.isPresent()) {
insert(block.get());
success = true;
}
}
}
if (dragboard.hasHtml() && !success) {
Optional<String> block = parserService.toWebImageBlock(dragboard.getHtml());
if (block.isPresent()) {
insert(block.get());
success = true;
}
}
if (dragboard.hasString() && !success) {
insert(dragboard.getString());
success = true;
}
event.setDropCompleted(success);
event.consume();
});
}
private void checkWordSuggestions() {
webEngine().executeScript("checkWordSuggestions()");
}
private void checkSpelling() {
webEngine().executeScript("checkSpelling()");
}
private String getSelectionOrAll() {
return (String) webEngine().executeScript("getSelectionOrAll()");
}
private void replaceSubs(Event event) {
String selection = getSelectionOrAll();
threadService.runTaskLater(() -> {
String result = controller.applyReplacements(selection);
if (Objects.equals(selection, result))
return;
threadService.runActionLater(() -> {
setEditorValue(getEditorValue().replace(selection, result));
});
});
}
@WebkitCall(from = "editor")
public void appendWildcard() {
threadService.runActionLater(() -> {
setChangedProperty(true);
});
}
public ObservableList<Runnable> getHandleReadyTasks() {
return handleReadyTasks;
}
public void setInitialEditorValue(String initialEditorValue) {
this.initialEditorValue = initialEditorValue;
}
public boolean isMarkdown() {
return is("markdown");
}
public boolean isAsciidoc() {
return is("asciidoc");
}
public Path getPath() {
return path;
}
public void setPath(Path path) {
this.path = path;
}
public FileTime getLastModifiedTime() {
return lastModifiedTime;
}
public void setLastModifiedTime(FileTime lastModifiedTime) {
this.lastModifiedTime = lastModifiedTime;
}
public boolean isHTML() {
return is("html");
}
public boolean getReady() {
return ready.get();
}
public BooleanProperty readyProperty() {
return ready;
}
public Path getSpellLanguage() {
return spellLanguage.get();
}
public ObjectProperty<Path> spellLanguageProperty() {
return spellLanguage;
}
public void setSpellLanguage(Path spellLanguage) {
this.spellLanguage.set(spellLanguage);
}
public void removeToLineStart() {
webEngine().executeScript("editor.removeToLineStart()");
}
public void addTypo(Token token) {
String tokenClass = token.isEmptySuggestion() ? "misspelled" : "misspelled-strong";
webEngine().executeScript(String.format("addTypo(%d,%d,%d,\"%s\")",
token.getRow(),
token.getStart(),
token.getEnd(),
tokenClass));
}
public void showSuggestions(List<String> suggestions) {
final ObservableList<MenuItem> contextMenuItems = contextMenu.getItems();
contextMenuItems.removeIf(m -> m.getStyleClass().contains("spell-suggestion"));
if (suggestions.isEmpty()) {
return;
}
final List<MenuItem> spells = new ArrayList<>();
for (String suggestion : suggestions) {
final MenuItem menuItem = MenuItemBuilt.item(suggestion)
.clazz("spell-suggestion").click(event -> {
this.replaceMisspelled(suggestion);
});
spells.add(menuItem);
}
final SeparatorMenuItem menuItem = new SeparatorMenuItem();
menuItem.getStyleClass().add("spell-suggestion");
spells.add(menuItem);
contextMenuItems.addAll(0, spells);
}
private void replaceMisspelled(String suggestion) {
webEngine().executeScript(String.format("replaceMisspelled(\"%s\")", suggestion));
}
public String tokenList() {
return (String) webEngine().executeScript("JSON.stringify(getTokenList())");
}
public void updateFoldStyle() {
FoldStyle foldStyle = editorConfigBean.getFoldStyle();
setFoldStyle(foldStyle);
}
public void setFoldStyle(FoldStyle style) {
if (Objects.isNull(style)) {
return;
}
threadService.runActionLater(() -> {
this.call("setFoldStyle", style.name().toLowerCase(Locale.ENGLISH));
});
}
public void updateCursorCoordinates() {
if (ready.get()) {
JSObject coordinates = (JSObject) this.call("getCursorCoordinates");
this.pageX = (Number) coordinates.getMember("pageX");
this.pageY = (Number) coordinates.getMember("pageY");
}
}
public boolean getChangedProperty() {
return changedProperty.get();
}
public BooleanProperty changedPropertyProperty() {
return changedProperty;
}
public void setChangedProperty(boolean changedProperty) {
this.changedProperty.set(changedProperty);
}
}