Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Refactored event listeners for newly added tabs, new tab selection, editor text changes, and caret paragraph changes. All panes are now decoupled.

Authordjarvis <email>
Date2016-12-11 12:20:02 GMT-0800
Commit6801a7fefe0d80504a26eade3eeac608f240b483
Parent1efcf56
Delta358 lines added, 466 lines removed, 108-line decrease
src/main/java/com/scrivenvar/processors/MarkdownCaretInsertionProcessor.java
import static com.scrivenvar.Constants.MD_CARET_POSITION;
import static java.lang.Character.isLetter;
-import javafx.beans.value.ObservableValue;
/**
public class MarkdownCaretInsertionProcessor extends AbstractProcessor<String> {
- private final ObservableValue<Integer> caretPosition;
+ private final int caretPosition;
/**
* Constructs a processor capable of inserting a caret marker into Markdown.
*
* @param processor The next processor in the chain.
* @param position The caret's current position in the text, cannot be null.
*/
public MarkdownCaretInsertionProcessor(
- final Processor<String> processor, final ObservableValue<Integer> position ) {
+ final Processor<String> processor, final int position ) {
super( processor );
this.caretPosition = position;
*/
private int getCaretPosition() {
- return this.caretPosition.getValue();
+ return this.caretPosition;
}
}
src/main/java/com/scrivenvar/processors/TextChangeProcessor.java
-/*
- * Copyright 2016 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors;
-
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableValue;
-
-/**
- * Responsible for forwarding change events to the document process chain. This
- * class isolates knowledge of the change events from the other processors.
- *
- * @author White Magic Software, Ltd.
- */
-public class TextChangeProcessor extends AbstractProcessor<String>
- implements ChangeListener<String> {
-
- /**
- * Constructs a new text processor that listens for changes to text and then
- * injects them into the processing chain.
- *
- * @param successor Usually the HTML Preview Processor.
- */
- public TextChangeProcessor( final Processor<String> successor ) {
- super( successor );
- }
-
- /**
- * Called when the text editor changes.
- *
- * @param observable Unused.
- * @param oldValue The value before being changed (unused).
- * @param newValue The value after being changed (passed to processChain).
- */
- @Override
- public void changed(
- final ObservableValue<? extends String> observable,
- final String oldValue,
- final String newValue ) {
- processChain( newValue );
- }
-
- /**
- * Performs no processing.
- *
- * @param t Returned value.
- *
- * @return t, without any processing.
- */
- @Override
- public String processLink( String t ) {
- return t;
- }
-}
src/main/java/com/scrivenvar/MainWindow.java
import static com.scrivenvar.Messages.get;
import com.scrivenvar.definition.DefinitionPane;
-import com.scrivenvar.editor.EditorPane;
-import com.scrivenvar.editor.MarkdownEditorPane;
-import com.scrivenvar.editor.VariableNameInjector;
-import com.scrivenvar.preview.HTMLPreviewPane;
-import com.scrivenvar.processors.HTMLPreviewProcessor;
-import com.scrivenvar.processors.MarkdownCaretInsertionProcessor;
-import com.scrivenvar.processors.MarkdownCaretReplacementProcessor;
-import com.scrivenvar.processors.MarkdownProcessor;
-import com.scrivenvar.processors.Processor;
-import com.scrivenvar.processors.TextChangeProcessor;
-import com.scrivenvar.processors.VariableProcessor;
-import com.scrivenvar.service.Options;
-import com.scrivenvar.util.Action;
-import com.scrivenvar.util.ActionUtils;
-import static com.scrivenvar.util.StageState.K_PANE_SPLIT_DEFINITION;
-import static com.scrivenvar.util.StageState.K_PANE_SPLIT_EDITOR;
-import static com.scrivenvar.util.StageState.K_PANE_SPLIT_PREVIEW;
-import com.scrivenvar.yaml.YamlParser;
-import com.scrivenvar.yaml.YamlTreeAdapter;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.prefs.Preferences;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.value.ObservableBooleanValue;
-import javafx.beans.value.ObservableValue;
-import javafx.collections.ListChangeListener.Change;
-import javafx.collections.ObservableList;
-import javafx.event.Event;
-import javafx.scene.Node;
-import javafx.scene.Scene;
-import javafx.scene.control.Alert;
-import javafx.scene.control.Alert.AlertType;
-import javafx.scene.control.Menu;
-import javafx.scene.control.MenuBar;
-import javafx.scene.control.SplitPane;
-import javafx.scene.control.Tab;
-import javafx.scene.control.ToolBar;
-import javafx.scene.control.TreeView;
-import javafx.scene.image.Image;
-import javafx.scene.image.ImageView;
-import static javafx.scene.input.KeyCode.ESCAPE;
-import javafx.scene.input.KeyEvent;
-import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
-import static javafx.scene.input.KeyEvent.KEY_PRESSED;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.layout.VBox;
-import javafx.stage.Window;
-import javafx.stage.WindowEvent;
-import org.fxmisc.richtext.StyleClassedTextArea;
-
-/**
- * Main window containing a tab pane in the center for file editors.
- *
- * @author Karl Tauber and White Magic Software, Ltd.
- */
-public class MainWindow {
-
- private final Options options = Services.load( Options.class );
-
- private Scene scene;
-
- private TreeView<String> treeView;
- private DefinitionPane definitionPane;
- private FileEditorTabPane fileEditorPane;
- private HTMLPreviewPane previewPane;
-
- private VariableNameInjector variableNameInjector;
-
- private YamlTreeAdapter yamlTreeAdapter;
- private YamlParser yamlParser;
-
- private MenuBar menuBar;
-
- public MainWindow() {
- initLayout();
- initTabsListener();
- restorePreferences();
- initEditorPaneListeners();
- initVariableNameInjector();
- }
-
- private void initLayout() {
- final SplitPane splitPane = new SplitPane(
- getDefinitionPane().getNode(),
- getFileEditorPane().getNode(),
- getPreviewPane().getNode() );
-
- splitPane.setDividerPositions(
- getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
- getFloat( K_PANE_SPLIT_EDITOR, .45f ),
- getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
-
- // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
- final BorderPane borderPane = new BorderPane();
- borderPane.setPrefSize( 1024, 800 );
- borderPane.setTop( createMenuBar() );
- borderPane.setCenter( splitPane );
-
- final Scene appScene = new Scene( borderPane );
- setScene( appScene );
- appScene.getStylesheets().add( Constants.STYLESHEET_PREVIEW );
- appScene.windowProperty().addListener(
- (observable, oldWindow, newWindow) -> {
- newWindow.setOnCloseRequest( e -> {
- if( !getFileEditorPane().closeAllEditors() ) {
- e.consume();
- }
- } );
-
- // Workaround JavaFX bug: deselect menubar if window loses focus.
- newWindow.focusedProperty().addListener(
- (obs, oldFocused, newFocused) -> {
- if( !newFocused ) {
- // Send an ESC key event to the menubar
- this.menuBar.fireEvent(
- new KeyEvent(
- KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE,
- false, false, false, false ) );
- }
- } );
- } );
- }
-
- private void initVariableNameInjector() {
- setVariableNameInjector( new VariableNameInjector(
- getFileEditorPane(),
- getDefinitionPane() )
- );
- }
-
- private Window getWindow() {
- return getScene().getWindow();
- }
-
- public Scene getScene() {
- return this.scene;
- }
-
- private void setScene( Scene scene ) {
- this.scene = scene;
- }
-
- /**
- * Creates a boolean property that is bound to another boolean value of the
- * active editor.
- */
- private BooleanProperty createActiveBooleanProperty(
- final Function<FileEditorTab, ObservableBooleanValue> func ) {
-
- final BooleanProperty b = new SimpleBooleanProperty();
- final FileEditorTab tab = getActiveFileEditor();
-
- if( tab != null ) {
- b.bind( func.apply( tab ) );
- }
-
- getFileEditorPane().activeFileEditorProperty().addListener(
- (observable, oldFileEditor, newFileEditor) -> {
- b.unbind();
-
- if( newFileEditor != null ) {
- b.bind( func.apply( newFileEditor ) );
- } else {
- b.set( false );
- }
- } );
-
- return b;
- }
-
- //---- File actions -------------------------------------------------------
- private void fileNew() {
- getFileEditorPane().newEditor();
- }
-
- private void fileOpen() {
- getFileEditorPane().openFileDialog();
- }
-
- private void fileClose() {
- getFileEditorPane().closeEditor( getActiveFileEditor(), true );
- }
-
- private void fileCloseAll() {
- getFileEditorPane().closeAllEditors();
- }
-
- private void fileSave() {
- getFileEditorPane().saveEditor( getActiveFileEditor() );
- }
-
- private void fileSaveAll() {
- getFileEditorPane().saveAllEditors();
- }
-
- private void fileExit() {
- final Window window = getWindow();
- Event.fireEvent( window,
- new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) );
- }
-
- //---- Help actions -------------------------------------------------------
- private void helpAbout() {
- Alert alert = new Alert( AlertType.INFORMATION );
- alert.setTitle( Messages.get( "Dialog.about.title" ) );
- alert.setHeaderText( Messages.get( "Dialog.about.header" ) );
- alert.setContentText( Messages.get( "Dialog.about.content" ) );
- alert.setGraphic( new ImageView( new Image( LOGO_32 ) ) );
- alert.initOwner( getWindow() );
-
- alert.showAndWait();
- }
-
- private FileEditorTabPane getFileEditorPane() {
- if( this.fileEditorPane == null ) {
- this.fileEditorPane = createFileEditorPane();
- }
-
- return this.fileEditorPane;
- }
-
- /**
- * Create an editor pane to hold file editor tabs.
- *
- * @return A new instance, never null.
- */
- private FileEditorTabPane createFileEditorPane() {
- return new FileEditorTabPane();
- }
-
- /**
- * Reloads the preferences from the previous load.
- */
- private void restorePreferences() {
- getFileEditorPane().restorePreferences();
- }
-
- private void initTabsListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Make sure the text processor kicks off when new files are opened.
- final ObservableList<Tab> tabs = editorPane.getTabs();
-
- // Update the preview pane on tab changes.
- tabs.addListener( (final Change<? extends Tab> change) -> {
- while( change.next() ) {
- if( change.wasAdded() ) {
- // Multiple tabs can be added simultaneously.
- for( final Tab newTab : change.getAddedSubList() ) {
- final FileEditorTab tab = (FileEditorTab)newTab;
-
- refresh( tab );
- }
- }
- }
- } );
- }
-
- private void initEditorPaneListeners() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Update the preview pane when moving the caret to a new paragraph.
- editorPane.addTabChangeListener(
- (ObservableValue<? extends Tab> tabPane,
- final Tab oldTab, final Tab newTab) -> {
-
- final FileEditorTab tab = (FileEditorTab)newTab;
-
- if( tab != null ) {
- getPreviewPane().setPath( tab.getPath() );
- refresh( tab );
- }
- } );
-
- editorPane.getEditor().textProperty().addListener( (ov, oldv, newv) -> {
- refresh( getActiveFileEditor() );
- } );
- }
-
- private void refresh( final FileEditorTab tab ) {
- System.out.println( "REFRESH: " + tab.getPath().toAbsolutePath() );
- }
-
- private MarkdownEditorPane getActiveEditor() {
- return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane());
- }
-
- private FileEditorTab getActiveFileEditor() {
- return getFileEditorPane().getActiveFileEditor();
- }
-
- private Processor<String> createVariableProcessor( final FileEditorTab tab ) {
- final HTMLPreviewPane previewPanel = getPreviewPane();
- final EditorPane editorPanel = tab.getEditorPane();
- final StyleClassedTextArea editor = editorPanel.getEditor();
-
- // TODO: Use a factory based on the filename extension. The default
- // extension will be for a markdown file (e.g., on file new).
- final Processor<String> hpp = new HTMLPreviewProcessor( previewPanel );
- final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp );
- final Processor<String> mp = new MarkdownProcessor( mcrp );
- final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, editor.caretPositionProperty() );
- final Processor<String> vp = new VariableProcessor( mcip, getResolvedMap() );
-
- return vp;
- }
-
- private TextChangeProcessor createTextChangeProcessor(
- final Processor<String> link ) {
- return new TextChangeProcessor( link );
- }
-
- /**
- * Listens for changes to tabs and their text editors.
- *
- * @see https://github.com/DaveJarvis/scrivenvar/issues/17
- * @see https://github.com/DaveJarvis/scrivenvar/issues/18
- *
- * @param tab The file editor tab that contains a text editor.
- */
- private void addListener( final FileEditorTab tab ) {
- final Processor<String> vnp = createVariableProcessor( tab );
- final TextChangeProcessor tcp = createTextChangeProcessor( vnp );
-
- addCaretParagraphListener( tab, vnp );
- }
-
- /**
- * When the caret changes paragraph, force re-rendering of the preview panel
- * using the chain-of-command.
- *
- * @param tab Contains a text editor to monitor for caret position changes.
- * @param vnp Called to re-process chain using the editor's content.
- */
- private void addCaretParagraphListener( final FileEditorTab tab, final Processor<String> vnp ) {
- final EditorPane editorPanel = tab.getEditorPane();
- final StyleClassedTextArea editor = editorPanel.getEditor();
-
- editorPanel.addCaretParagraphListener(
- (final ObservableValue<? extends Integer> observable,
- final Integer oldValue, final Integer newValue) -> {
-
- // Kick off the processing chain at the variable processor when the
- // cursor changes paragraphs. This might cause some slight duplication
- // when the Enter key is pressed.
- vnp.processChain( editor.getText() );
- } );
- }
-
- protected DefinitionPane createDefinitionPane() {
- return new DefinitionPane( getTreeView() );
- }
-
- private DefinitionPane getDefinitionPane() {
- if( this.definitionPane == null ) {
- this.definitionPane = createDefinitionPane();
- }
-
- return this.definitionPane;
- }
-
- public MenuBar getMenuBar() {
- return menuBar;
+import com.scrivenvar.editor.MarkdownEditorPane;
+import com.scrivenvar.editor.VariableNameInjector;
+import com.scrivenvar.preview.HTMLPreviewPane;
+import com.scrivenvar.processors.HTMLPreviewProcessor;
+import com.scrivenvar.processors.MarkdownCaretInsertionProcessor;
+import com.scrivenvar.processors.MarkdownCaretReplacementProcessor;
+import com.scrivenvar.processors.MarkdownProcessor;
+import com.scrivenvar.processors.Processor;
+import com.scrivenvar.processors.VariableProcessor;
+import com.scrivenvar.service.Options;
+import com.scrivenvar.util.Action;
+import com.scrivenvar.util.ActionUtils;
+import static com.scrivenvar.util.StageState.K_PANE_SPLIT_DEFINITION;
+import static com.scrivenvar.util.StageState.K_PANE_SPLIT_EDITOR;
+import static com.scrivenvar.util.StageState.K_PANE_SPLIT_PREVIEW;
+import com.scrivenvar.yaml.YamlParser;
+import com.scrivenvar.yaml.YamlTreeAdapter;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.prefs.Preferences;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ObservableBooleanValue;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ListChangeListener.Change;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuBar;
+import javafx.scene.control.SplitPane;
+import javafx.scene.control.Tab;
+import javafx.scene.control.ToolBar;
+import javafx.scene.control.TreeView;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import static javafx.scene.input.KeyCode.ESCAPE;
+import javafx.scene.input.KeyEvent;
+import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
+import static javafx.scene.input.KeyEvent.KEY_PRESSED;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import javafx.stage.Window;
+import javafx.stage.WindowEvent;
+
+/**
+ * Main window containing a tab pane in the center for file editors.
+ *
+ * @author Karl Tauber and White Magic Software, Ltd.
+ */
+public class MainWindow {
+
+ private final Options options = Services.load( Options.class );
+
+ private Scene scene;
+
+ private TreeView<String> treeView;
+ private DefinitionPane definitionPane;
+ private FileEditorTabPane fileEditorPane;
+ private HTMLPreviewPane previewPane;
+
+ private VariableNameInjector variableNameInjector;
+
+ private YamlTreeAdapter yamlTreeAdapter;
+ private YamlParser yamlParser;
+
+ private MenuBar menuBar;
+
+ public MainWindow() {
+ initLayout();
+ initTabAddedListener();
+ restorePreferences();
+ initTabChangeListener();
+ initVariableNameInjector();
+ }
+
+ private void initLayout() {
+ final SplitPane splitPane = new SplitPane(
+ getDefinitionPane().getNode(),
+ getFileEditorPane().getNode(),
+ getPreviewPane().getNode() );
+
+ splitPane.setDividerPositions(
+ getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
+ getFloat( K_PANE_SPLIT_EDITOR, .45f ),
+ getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
+
+ // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
+ final BorderPane borderPane = new BorderPane();
+ borderPane.setPrefSize( 1024, 800 );
+ borderPane.setTop( createMenuBar() );
+ borderPane.setCenter( splitPane );
+
+ final Scene appScene = new Scene( borderPane );
+ setScene( appScene );
+ appScene.getStylesheets().add( Constants.STYLESHEET_PREVIEW );
+ appScene.windowProperty().addListener(
+ (observable, oldWindow, newWindow) -> {
+ newWindow.setOnCloseRequest( e -> {
+ if( !getFileEditorPane().closeAllEditors() ) {
+ e.consume();
+ }
+ } );
+
+ // Workaround JavaFX bug: deselect menubar if window loses focus.
+ newWindow.focusedProperty().addListener(
+ (obs, oldFocused, newFocused) -> {
+ if( !newFocused ) {
+ // Send an ESC key event to the menubar
+ this.menuBar.fireEvent(
+ new KeyEvent(
+ KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE,
+ false, false, false, false ) );
+ }
+ } );
+ } );
+ }
+
+ private void initVariableNameInjector() {
+ setVariableNameInjector( new VariableNameInjector(
+ getFileEditorPane(),
+ getDefinitionPane() )
+ );
+ }
+
+ private Window getWindow() {
+ return getScene().getWindow();
+ }
+
+ public Scene getScene() {
+ return this.scene;
+ }
+
+ private void setScene( Scene scene ) {
+ this.scene = scene;
+ }
+
+ /**
+ * Creates a boolean property that is bound to another boolean value of the
+ * active editor.
+ */
+ private BooleanProperty createActiveBooleanProperty(
+ final Function<FileEditorTab, ObservableBooleanValue> func ) {
+
+ final BooleanProperty b = new SimpleBooleanProperty();
+ final FileEditorTab tab = getActiveFileEditor();
+
+ if( tab != null ) {
+ b.bind( func.apply( tab ) );
+ }
+
+ getFileEditorPane().activeFileEditorProperty().addListener(
+ (observable, oldFileEditor, newFileEditor) -> {
+ b.unbind();
+
+ if( newFileEditor != null ) {
+ b.bind( func.apply( newFileEditor ) );
+ } else {
+ b.set( false );
+ }
+ } );
+
+ return b;
+ }
+
+ //---- File actions -------------------------------------------------------
+ private void fileNew() {
+ getFileEditorPane().newEditor();
+ }
+
+ private void fileOpen() {
+ getFileEditorPane().openFileDialog();
+ }
+
+ private void fileClose() {
+ getFileEditorPane().closeEditor( getActiveFileEditor(), true );
+ }
+
+ private void fileCloseAll() {
+ getFileEditorPane().closeAllEditors();
+ }
+
+ private void fileSave() {
+ getFileEditorPane().saveEditor( getActiveFileEditor() );
+ }
+
+ private void fileSaveAll() {
+ getFileEditorPane().saveAllEditors();
+ }
+
+ private void fileExit() {
+ final Window window = getWindow();
+ Event.fireEvent( window,
+ new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) );
+ }
+
+ //---- Help actions -------------------------------------------------------
+ private void helpAbout() {
+ Alert alert = new Alert( AlertType.INFORMATION );
+ alert.setTitle( Messages.get( "Dialog.about.title" ) );
+ alert.setHeaderText( Messages.get( "Dialog.about.header" ) );
+ alert.setContentText( Messages.get( "Dialog.about.content" ) );
+ alert.setGraphic( new ImageView( new Image( LOGO_32 ) ) );
+ alert.initOwner( getWindow() );
+
+ alert.showAndWait();
+ }
+
+ private FileEditorTabPane getFileEditorPane() {
+ if( this.fileEditorPane == null ) {
+ this.fileEditorPane = createFileEditorPane();
+ }
+
+ return this.fileEditorPane;
+ }
+
+ /**
+ * Create an editor pane to hold file editor tabs.
+ *
+ * @return A new instance, never null.
+ */
+ private FileEditorTabPane createFileEditorPane() {
+ return new FileEditorTabPane();
+ }
+
+ /**
+ * Reloads the preferences from the previous load.
+ */
+ private void restorePreferences() {
+ getFileEditorPane().restorePreferences();
+ }
+
+ private void initTabAddedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Make sure the text processor kicks off when new files are opened.
+ final ObservableList<Tab> tabs = editorPane.getTabs();
+
+ // Update the preview pane on tab changes.
+ tabs.addListener( (final Change<? extends Tab> change) -> {
+ while( change.next() ) {
+ if( change.wasAdded() ) {
+ // Multiple tabs can be added simultaneously.
+ for( final Tab newTab : change.getAddedSubList() ) {
+ final FileEditorTab tab = (FileEditorTab)newTab;
+
+ initTextChangeListener( tab );
+ initCaretParagraphListener( tab );
+ process( tab );
+ }
+ }
+ }
+ } );
+ }
+
+ /**
+ * Listen for tab changes.
+ */
+ private void initTabChangeListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Update the preview pane changing tabs.
+ editorPane.addTabChangeListener(
+ (ObservableValue<? extends Tab> tabPane,
+ final Tab oldTab, final Tab newTab) -> {
+
+ final FileEditorTab tab = (FileEditorTab)newTab;
+
+ if( tab != null ) {
+ // When a new tab is selected, ensure that the base path to images
+ // is set correctly.
+ getPreviewPane().setPath( tab.getPath() );
+ process( tab );
+ }
+ } );
+ }
+
+ private void initTextChangeListener( final FileEditorTab tab ) {
+ tab.addTextChangeListener( (ObservableValue<? extends String> editor,
+ final String oldValue, final String newValue) -> {
+ process( tab );
+ } );
+ }
+
+ private void initCaretParagraphListener( final FileEditorTab tab ) {
+ tab.addCaretParagraphListener( (ObservableValue<? extends Integer> editor,
+ final Integer oldValue, final Integer newValue) -> {
+ process( tab );
+ } );
+ }
+
+ /**
+ * Called whenever the preview pane becomes out of sync with the file editor
+ * tab. This can be called when the text changes, the caret paragraph changes,
+ * or the file tab changes.
+ *
+ * @param tab The file editor tab that has been changed in some fashion.
+ */
+ private void process( final FileEditorTab tab ) {
+ // TODO: Use a factory based on the filename extension. The default
+ // extension will be for a markdown file (e.g., on file new).
+ final Processor<String> hpp = new HTMLPreviewProcessor( getPreviewPane() );
+ final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp );
+ final Processor<String> mp = new MarkdownProcessor( mcrp );
+ final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, tab.getCaretPosition() );
+ final Processor<String> vp = new VariableProcessor( mcip, getResolvedMap() );
+
+ vp.processChain( tab.getEditorText() );
+ }
+
+ private MarkdownEditorPane getActiveEditor() {
+ return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane());
+ }
+
+ private FileEditorTab getActiveFileEditor() {
+ return getFileEditorPane().getActiveFileEditor();
+ }
+
+ protected DefinitionPane createDefinitionPane() {
+ return new DefinitionPane( getTreeView() );
+ }
+
+ private DefinitionPane getDefinitionPane() {
+ if( this.definitionPane == null ) {
+ this.definitionPane = createDefinitionPane();
+ }
+
+ return this.definitionPane;
+ }
+
+ public MenuBar getMenuBar() {
+ return this.menuBar;
}