/** * Copyright [2015] [Christian Loehnert] * * Licensed 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. */ package de.ks.markdown.editor; import de.ks.activity.ActivityController; import de.ks.activity.initialization.ActivityCallback; import de.ks.activity.initialization.ActivityInitialization; import de.ks.application.fxml.DefaultLoader; import de.ks.executor.group.LastExecutionGroup; import de.ks.i18n.Localized; import de.ks.javafx.ScreenResolver; import de.ks.markdown.viewer.MarkdownContent; import de.ks.markdown.viewer.MarkdownViewer; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.geometry.Rectangle2D; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.control.TextArea; import javafx.scene.input.KeyCode; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.scene.web.WebView; import javafx.stage.Modality; import javafx.stage.Stage; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.enterprise.inject.spi.CDI; import javax.inject.Inject; import java.awt.*; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URL; import java.util.Collections; import java.util.ResourceBundle; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; public class MarkdownEditor implements Initializable, ActivityCallback { public static CompletableFuture<DefaultLoader<Node, MarkdownEditor>> load(Consumer<StackPane> viewConsumer, Consumer<MarkdownEditor> controllerConsumer) { ActivityInitialization initialization = CDI.current().select(ActivityInitialization.class).get(); return initialization.loadAdditionalControllerWithFuture(MarkdownEditor.class)// .thenApply(loader -> { viewConsumer.accept((StackPane) loader.getView()); controllerConsumer.accept(loader.getController()); return loader; }); } private static final Logger log = LoggerFactory.getLogger(MarkdownEditor.class); @Inject ActivityController controller; @Inject ActivityInitialization initialization; @FXML protected TextArea editor; @FXML protected Tab previewTab; @FXML protected Button help; @FXML protected GridPane mainPane; @FXML protected TabPane tabPane; @FXML protected StackPane root; @FXML protected HBox editorCommandPane; @FXML protected StackPane editorContainer; @FXML protected TextArea plainHtml; protected File lastFile; protected final SimpleStringProperty text = new SimpleStringProperty(); protected final SimpleObjectProperty<File> file = new SimpleObjectProperty(); protected LastExecutionGroup<String> renderGroup; protected Button insertImage = null; protected boolean focusOnEditor = true; protected Stage previewPopupStage; protected MarkdownViewer preview; protected MarkdownViewer popupPreview; protected Node previewNode; protected Node popupPreviewNode; @Override public void initialize(URL location, ResourceBundle resources) { initializePreview(); initializePopupPreview(); renderGroup = new LastExecutionGroup<>("markdownrender", 500, controller.getExecutorService()); text.bindBidirectional(editor.textProperty()); editor.textProperty().addListener((p, o, n) -> { if (n != null) { renderGroup.schedule(() -> n)// .thenApplyAsync(s -> { return s; }, controller.getExecutorService())// .thenAcceptAsync(s -> { if (previewTab.isSelected()) { preview.clear(); preview.showDirect(s); } else { preview.preload(Collections.singletonList(new MarkdownContent(MarkdownViewer.DEFAULT, s))); } if (previewPopupStage != null) { popupPreview.clear(); popupPreview.showDirect(s); } }, controller.getJavaFXExecutor()); } }); tabPane.focusedProperty().addListener((p, o, n) -> { if (n) { if (tabPane.getSelectionModel().getSelectedIndex() == 0) { if (focusOnEditor) { editor.requestFocus(); } } } else { focusOnEditor = true; } }); tabPane.getSelectionModel().selectedIndexProperty().addListener((p, o, n) -> { if (n != null && n.intValue() == 1) { controller.getJavaFXExecutor().submit(() -> preview.requestFocus()); preview.show(new MarkdownContent(MarkdownViewer.DEFAULT, editor.getText())); } if (o == null || n == null) { return; } if (o.intValue() != 0 && n.intValue() == 0) { controller.getJavaFXExecutor().submit(() -> editor.requestFocus()); } }); editor.setOnKeyPressed(e -> { KeyCode code = e.getCode(); if (e.getCode() == KeyCode.P && e.isControlDown()) { showPreviewPopup(); e.consume(); } }); } protected void initializePreview() { DefaultLoader<Node, MarkdownViewer> previewLoader = initialization.loadAdditionalController(MarkdownViewer.class); previewNode = previewLoader.getView(); previewTab.setContent(previewNode); preview = previewLoader.getController(); plainHtml.textProperty().bind(preview.currentHtmlProperty()); preview.fileProperty().bind(file); } protected void initializePopupPreview() { DefaultLoader<Node, MarkdownViewer> previewLoader = initialization.loadAdditionalController(MarkdownViewer.class); popupPreviewNode = previewLoader.getView(); popupPreview = previewLoader.getController(); popupPreview.fileProperty().bind(file); } @FXML void showHelp() { String title = Localized.get("help"); title = StringUtils.remove(title, "_"); new Thread(() -> { Desktop desktop = Desktop.getDesktop(); URI uri = URI.create("http://daringfireball.net/projects/markdown/syntax"); try { desktop.browse(uri); } catch (IOException e) { log.error("Could not browse {}", uri, e); } }).start(); } @FXML void showPreviewPopup() { if (previewPopupStage == null) { String title = Localized.get("markdown.preview"); previewPopupStage = new Stage(); previewPopupStage.setTitle(title); Scene scene = new Scene(new StackPane(popupPreviewNode)); scene.setOnKeyReleased(e -> { if (e.getCode() == KeyCode.ESCAPE) { previewPopupStage.close(); } }); previewPopupStage.setScene(scene); Rectangle2D bounds = new ScreenResolver().getScreenToShow().getBounds(); previewPopupStage.setX(bounds.getMinX()); previewPopupStage.setY(bounds.getMinY()); previewPopupStage.setWidth(bounds.getWidth()); previewPopupStage.setHeight(bounds.getHeight()); previewPopupStage.initModality(Modality.NONE); previewPopupStage.setOnShowing(e -> { popupPreview.showDirect(getText()); }); previewPopupStage.setOnCloseRequest(e -> this.previewPopupStage = null); previewPopupStage.show(); } } public SimpleStringProperty textProperty() { return text; } public String getText() { return text.getValueSafe(); } public void setText(String text) { this.editor.setText(text); } public TextArea getEditor() { return editor; } public void selectPreview() { this.tabPane.getSelectionModel().select(1); } public void selectEditor() { this.tabPane.getSelectionModel().select(0); } public WebView getPreview() { return preview.getWebView(); } public File getFile() { return file.get(); } public SimpleObjectProperty<File> fileProperty() { return file; } public void setFile(File file) { this.file.set(file); } @Override public void onSuspend() { controller.getJavaFXExecutor().invokeInJavaFXThread(() -> { if (previewPopupStage != null) { previewPopupStage.close(); } return null; }); } @Override public void onStop() { onSuspend(); } }