package com.zestedesavoir.zestwriter.view; import com.zestedesavoir.zestwriter.MainApp; import com.zestedesavoir.zestwriter.model.ContentNode; import com.zestedesavoir.zestwriter.model.Textual; import com.zestedesavoir.zestwriter.utils.Configuration; import com.zestedesavoir.zestwriter.utils.Corrector; import com.zestedesavoir.zestwriter.utils.readability.Readability; import com.zestedesavoir.zestwriter.view.com.FunctionTreeFactory; import javafx.application.Platform; import javafx.beans.property.BooleanPropertyBase; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableValue; import javafx.concurrent.Service; import javafx.concurrent.Task; import javafx.concurrent.Worker.State; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.control.Label; import javafx.scene.control.SplitPane; import javafx.scene.control.Tab; import javafx.scene.control.TreeItem; import javafx.scene.layout.BorderPane; import javafx.scene.layout.ColumnConstraints; import javafx.scene.web.WebView; import lombok.Getter; import netscape.javascript.JSException; import org.fxmisc.flowless.VirtualizedScrollPane; import org.fxmisc.richtext.LineNumberFactory; import org.fxmisc.richtext.StyleClassedTextArea; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.regex.Pattern; public class MdConvertController { public static final Pattern recognizeNumber = Pattern.compile("^(\\s*)([\\d][\\.]) (\\s*)(.*)"); public static final Pattern recognizeBullet = Pattern.compile("^(\\s*)([*|-]) (\\s*)(.*)"); public static Corrector corrector; private final Logger logger = LoggerFactory.getLogger(MdConvertController.class); private MdTextController mdBox; private Service<String> renderTask; private int xRenderPosition = 0; private int yRenderPosition = 0; private StringProperty countChars = new SimpleStringProperty(); private StringProperty countWords = new SimpleStringProperty(); private StringProperty countTimes = new SimpleStringProperty(); private BooleanPropertyBase needRefresh = new SimpleBooleanProperty(false); @Getter private BooleanPropertyBase saved = new SimpleBooleanProperty(true); @FXML private WebView renderView; @FXML private SplitPane splitPane; @FXML private BorderPane boxRender; @FXML private Tab tab; private StyleClassedTextArea sourceText; @FXML private BorderPane container; VirtualizedScrollPane<StyleClassedTextArea> vsPane; public void setMdBox(MdTextController mdBox, Textual extract) { this.mdBox = mdBox; initCurrentComponents(extract); tab.setText(extract.getTitle()); Platform.runLater(() -> { saved.addListener((observableValue, s, s2) -> { if(s2) { tab.setText(extract.getTitle()); } else { tab.setText("! " + extract.getTitle()); } }); sourceText.replaceText(extract.getMarkdown()); initStats(); sourceText.getUndoManager().forgetHistory(); sourceText.richChanges() .filter(ch -> !ch.getInserted().equals(ch.getRemoved())) .subscribe(change -> { sourceText.getUndoManager().mark(); getMdBox().setCurrentSaved(false); if(renderTask.getState().equals(State.READY)) { renderTask.start(); needRefresh.set(false); } else { needRefresh.set(true); } }); if(renderTask.getState().equals(State.READY)) { renderTask.start(); } }); tab.setOnSelectionChanged(t -> { initCurrentComponents(extract); TreeItem<ContentNode> selected = mdBox.selectItemOnTree(mdBox.getSummary().getRoot(), extract); Platform.runLater(() -> { if(selected != null) { mdBox.getSummary().getSelectionModel().select(selected); } sourceText.requestFocus(); initStats(); }); }); } private void initCurrentComponents(Textual extract) { mdBox.currentSaved = saved; mdBox.setCurrentRenderView(renderView); mdBox.setCurrentSourceText(sourceText); mdBox.setCurrentBoxRender(boxRender); mdBox.setCurrentExtract(extract); mdBox.getSaveButton().disableProperty().bind(saved); mdBox.initKeyMapping(sourceText); } public MdTextController getMdBox() { return mdBox; } public MdConvertController() { sourceText = new StyleClassedTextArea(); sourceText.setStyle("markdown-editor"); sourceText.setWrapText(true); vsPane = new VirtualizedScrollPane<>(sourceText); } @FXML private void initialize() { container.setCenter(vsPane); sourceText.getStylesheets().add(MainApp.class.getResource("css/editor.css").toExternalForm()); sourceText.setStyle("-fx-font-family: \"" + MainApp.getConfig().getEditorFont() + "\";-fx-font-size: " + MainApp.getConfig().getEditorFontsize() + ";"); if(MainApp.getConfig().isEditorLinenoView()) sourceText.setParagraphGraphicFactory(LineNumberFactory.get(sourceText)); if(MainApp.getConfig().isEditorRenderView()) { initRenderTask(); } else { splitPane.getItems().remove(1); } Platform.runLater(sourceText::requestFocus); } private void initRenderTask() { renderTask = new Service<String>() { @Override protected Task<String> createTask() { return new Task<String>() { @Override protected String call() throws Exception { String html = getMdBox().markdownToHtml(sourceText.getText()); if (html != null) { return MainApp.getMdUtils().addHeaderAndFooter(html); } else { throw new IOException(); } } }; } }; renderTask.setOnFailed(t -> { renderTask.restart(); }); renderTask.setOnSucceeded(t -> { Platform.runLater(() -> { yRenderPosition = getVScrollValue(renderView); xRenderPosition = getHScrollValue(renderView); renderView.getEngine().loadContent(renderTask.getValue()); performStats(); renderTask.reset(); if(needRefresh.getValue()) { needRefresh.set(false); renderTask.start(); } }); }); renderView.getEngine().getLoadWorker().stateProperty() .addListener((ObservableValue<? extends State> ov, State oldState, State newState) -> { if (newState == State.SUCCEEDED) { Platform.runLater(() -> scrollTo(renderView, xRenderPosition, yRenderPosition)); } }); } public void performStats() { Readability readText = new Readability(sourceText.getText()); countChars.setValue(Configuration.getBundle().getString("ui.statusbar.stats.chars") + readText.getCharacters()); countWords.setValue(Configuration.getBundle().getString("ui.statusbar.stats.words") + readText.getWords()); countTimes.setValue(FunctionTreeFactory.getNumberOfTextualReadMinutes(sourceText.getText())); } public void initStats() { String fontSize="-fx-font-size: 0.9em;"; getMdBox().getMainApp().getMenuController().getHBottomBox().getChildren().clear(); getMdBox().getMainApp().getMenuController().getHBottomBox().getColumnConstraints().clear(); getMdBox().getMainApp().getMenuController().getHBottomBox().setPadding(new Insets(5, 5, 5, 5)); ColumnConstraints c1 = new ColumnConstraints(); ColumnConstraints c2 = new ColumnConstraints(); ColumnConstraints c3 = new ColumnConstraints(); ColumnConstraints c4 = new ColumnConstraints(); c1.setPercentWidth(50); c2.setPercentWidth(20); c3.setPercentWidth(15); c4.setPercentWidth(15); Label chars = new Label(); Label words = new Label(); Label times = new Label(); chars.setStyle(fontSize); words.setStyle(fontSize); times.setStyle(fontSize); getMdBox().getMainApp().getMenuController().getHBottomBox().getColumnConstraints().addAll(c1, c2, c3, c4); getMdBox().getMainApp().getMenuController().getHBottomBox().add(times, 1, 0); getMdBox().getMainApp().getMenuController().getHBottomBox().add(chars, 2, 0); getMdBox().getMainApp().getMenuController().getHBottomBox().add(words, 3, 0); chars.textProperty().bind(countChars); words.textProperty().bind(countWords); times.textProperty().bind(countTimes); performStats(); } /** * Scrolls to the specified position. * * @param view web view that shall be scrolled * @param x horizontal scroll value * @param y vertical scroll value */ public void scrollTo(WebView view, int x, int y) { view.getEngine().executeScript("window.scrollTo(" + x + ", " + y + ")"); } /** * Returns the vertical scroll value, i.e. thumb position. This is * equivalent to {@link javafx.scene.control.ScrollBar#getValue()}. * * @param view web view that shall be scrolled * @return vertical scroll value */ public int getVScrollValue(WebView view) { try { return (Integer) view.getEngine().executeScript("document.body.scrollTop"); } catch(JSException e) { logger.trace(e.getMessage(), e); return 0; } } /** * Returns the horizontal scroll value, i.e. thumb position. This is * equivalent to {@link javafx.scene.control.ScrollBar#getValue()}. * * @param view * @return horizontal scroll value */ public int getHScrollValue(WebView view) { try { return (Integer) view.getEngine().executeScript("document.body.scrollLeft"); } catch(JSException e) { logger.trace(e.getMessage(), e); return 0; } } }