Dave Jarvis' Repositories

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

Add filename to definition pane

AuthorDaveJarvis <email>
Date2020-06-10 00:28:55 GMT-0700
Commit507adeacc5bf8628f59e097ddfae28f0c093571b
Parent5224696
Delta1166 lines added, 1140 lines removed, 26-line increase
src/main/java/com/scrivenvar/definition/DefinitionPane.java
package com.scrivenvar.definition;
-import com.scrivenvar.AbstractPane;
-import javafx.collections.ObservableList;
-import javafx.event.Event;
-import javafx.event.EventHandler;
-import javafx.scene.Node;
-import javafx.scene.control.*;
-import javafx.scene.control.cell.TextFieldTreeCell;
-import javafx.scene.input.KeyEvent;
-import javafx.util.StringConverter;
-
-import java.util.*;
-
-import static com.scrivenvar.Messages.get;
-import static javafx.scene.input.KeyEvent.KEY_PRESSED;
-
-/**
- * Provides the user interface that holdsa {@link TreeView}, which
- * allows users to interact with key/value pairs loaded from the
- * {@link DocumentParser} and adapted using a {@link TreeAdapter}.
- *
- * @author White Magic Software, Ltd.
- */
-public final class DefinitionPane extends AbstractPane {
-
- /**
- * Trimmed off the end of a word to match a variable name.
- */
- private final static String TERMINALS = ":;,.!?-/\\¡¿";
-
- /**
- * Contains a view of the definitions.
- */
- private final TreeView<String> mTreeView = new TreeView<>();
-
- /**
- * Handlers for key press events.
- */
- private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
- = new HashSet<>();
-
- /**
- * Constructs a definition pane with a given tree view root.
- */
- public DefinitionPane() {
- final var treeView = getTreeView();
- treeView.setEditable( true );
- treeView.setCellFactory( cell -> createTreeCell() );
- treeView.setContextMenu( createContextMenu() );
- treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
- treeView.setShowRoot( false );
- getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
- }
-
- /**
- * Changes the root of the {@link TreeView} to the root of the
- * {@link TreeView} from the {@link DefinitionSource}.
- *
- * @param definitionSource Container for the hierarchy of key/value pairs
- * to replace the existing hierarchy.
- */
- public void update( final DefinitionSource definitionSource ) {
- assert definitionSource != null;
-
- final TreeAdapter treeAdapter = definitionSource.getTreeAdapter();
- final TreeItem<String> root = treeAdapter.adapt(
- get( "Pane.definition.node.root.title" )
- );
-
- getTreeView().setRoot( root );
- }
-
- public Map<String, String> toMap() {
- return TreeItemAdapter.toMap( getTreeView().getRoot() );
- }
-
- /**
- * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
- * is modified. The modifications include: item value changes, item additions,
- * and item removals.
- * <p>
- * Safe to call multiple times; if a handler is already registered, the
- * old handler is used.
- * </p>
- *
- * @param handler The handler to call whenever any {@link TreeItem} changes.
- */
- public void addTreeChangeHandler(
- final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
- final TreeItem<String> root = getTreeView().getRoot();
- root.addEventHandler( TreeItem.valueChangedEvent(), handler );
- root.addEventHandler( TreeItem.childrenModificationEvent(), handler );
- }
-
- public void addKeyEventHandler(
- final EventHandler<? super KeyEvent> handler ) {
- getKeyEventHandlers().add( handler );
- }
-
- /**
- * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
- * well-formed for export. A tree is considered well-formed if the following
- * conditions are met:
- *
- * <ul>
- * <li>The root node contains at least one child node having a leaf.</li>
- * <li>There are no leaf nodes with sibling leaf nodes.</li>
- * </ul>
- *
- * @return {@code null} if the document is well-formed, otherwise the
- * problematic child {@link TreeItem}.
- */
- public TreeItem<String> isTreeWellFormed() {
- final var root = getTreeView().getRoot();
-
- for( final var child : root.getChildren() ) {
- final var problemChild = isWellFormed( child );
-
- if( child.isLeaf() || problemChild != null ) {
- return problemChild;
- }
- }
-
- return null;
- }
-
- /**
- * Determines whether the document is well-formed by ensuring that
- * child branches do not contain multiple leaves.
- *
- * @param item The sub-tree to check for well-formedness.
- * @return {@code null} when the tree is well-formed, otherwise the
- * problematic {@link TreeItem}.
- */
- private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
- int childLeafs = 0;
- int childBranches = 0;
-
- for( final TreeItem<String> child : item.getChildren() ) {
- if( child.isLeaf() ) {
- childLeafs++;
- }
- else {
- childBranches++;
- }
-
- final var problemChild = isWellFormed( child );
-
- if( problemChild != null ) {
- return problemChild;
- }
- }
-
- return ((childBranches > 0 && childLeafs == 0) ||
- (childBranches == 0 && childLeafs <= 1)) ? null : item;
- }
-
- /**
- * Returns the leaf that matches the given value. If the value is terminally
- * punctuated, the punctuation is removed if no match was found.
- *
- * @param value The value to find, never null.
- * @param findMode Defines how to match words.
- * @return The leaf that contains the given value, or null if neither the
- * original value nor the terminally-trimmed value was found.
- */
- public VariableTreeItem<String> findLeaf(
- final String value, final FindMode findMode ) {
- final VariableTreeItem<String> root = getTreeRoot();
- final VariableTreeItem<String> leaf = root.findLeaf( value, findMode );
-
- return leaf == null
- ? root.findLeaf( rtrimTerminalPunctuation( value ) )
- : leaf;
- }
-
- /**
- * Removes punctuation from the end of a string.
- *
- * @param s The string to trim, never null.
- * @return The string trimmed of all terminal characters from the end
- */
- private String rtrimTerminalPunctuation( final String s ) {
- assert s != null;
- int index = s.length() - 1;
-
- while( index > 0 && (TERMINALS.indexOf( s.charAt( index ) ) >= 0) ) {
- index--;
- }
-
- return s.substring( 0, index );
- }
-
- /**
- * Expands the node to the root, recursively.
- *
- * @param <T> The type of tree item to expand (usually String).
- * @param node The node to expand.
- */
- public <T> void expand( final TreeItem<T> node ) {
- if( node != null ) {
- expand( node.getParent() );
-
- if( !node.isLeaf() ) {
- node.setExpanded( true );
- }
- }
- }
-
- public void select( final TreeItem<String> item ) {
- getSelectionModel().clearSelection();
- getSelectionModel().select( getTreeView().getRow( item ) );
- }
-
- /**
- * Collapses the tree, recursively.
- */
- public void collapse() {
- collapse( getTreeRoot().getChildren() );
- }
-
- /**
- * Collapses the tree, recursively.
- *
- * @param <T> The type of tree item to expand (usually String).
- * @param nodes The nodes to collapse.
- */
- private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
- for( final TreeItem<T> node : nodes ) {
- node.setExpanded( false );
- collapse( node.getChildren() );
- }
- }
-
- /**
- * @return {@code true} when the user is editing a {@link TreeItem}.
- */
- private boolean isEditingTreeItem() {
- return getTreeView().editingItemProperty().getValue() != null;
- }
-
- /**
- * Changes to edit mode for the selected item.
- */
- private void editSelectedItem() {
- getTreeView().edit( getSelectedItem() );
- }
-
- /**
- * Removes all selected items from the {@link TreeView}.
- */
- private void deleteSelectedItems() {
- for( final TreeItem<String> item : getSelectedItems() ) {
- final TreeItem<String> parent = item.getParent();
-
- if( parent != null ) {
- parent.getChildren().remove( item );
- }
- }
- }
-
- /**
- * Deletes the selected item.
- */
- private void deleteSelectedItem() {
- final TreeItem<String> c = getSelectedItem();
- getSiblings( c ).remove( c );
- }
-
- /**
- * Adds a new item under the selected item (or root if nothing is selected).
- * There are a few conditions to consider: when adding to the root,
- * when adding to a leaf, and when adding to a non-leaf. Items added to the
- * root must contain two items: a key and a value.
- */
- private void addItem() {
- final TreeItem<String> value = createTreeItem();
- getSelectedItem().getChildren().add( value );
- expand( value );
- select( value );
- }
-
- private ContextMenu createContextMenu() {
- final ContextMenu menu = new ContextMenu();
- final ObservableList<MenuItem> items = menu.getItems();
-
- addMenuItem( items, "Definition.menu.create" )
- .setOnAction( e -> addItem() );
-
- addMenuItem( items, "Definition.menu.rename" )
- .setOnAction( e -> editSelectedItem() );
-
- addMenuItem( items, "Definition.menu.remove" )
- .setOnAction( e -> deleteSelectedItem() );
-
- return menu;
- }
-
- /**
- * Executes hot-keys for edits to the definition tree.
- *
- * @param event Contains the key code of the key that was pressed.
- */
- private void keyEventFilter( final KeyEvent event ) {
- if( !isEditingTreeItem() ) {
- switch( event.getCode() ) {
- case ENTER:
- expand( getSelectedItem() );
- event.consume();
- break;
-
- case DELETE:
- deleteSelectedItems();
- break;
-
- case INSERT:
- addItem();
- break;
-
- case R:
- if( event.isControlDown() ) {
- editSelectedItem();
- }
-
- break;
- }
-
- for( final var handler : getKeyEventHandlers() ) {
- handler.handle( event );
- }
- }
- }
-
- /**
- * Adds a menu item to a list of menu items.
- *
- * @param items The list of menu items to append to.
- * @param labelKey The resource bundle key name for the menu item's label.
- * @return The menu item added to the list of menu items.
- */
- private MenuItem addMenuItem(
- final List<MenuItem> items, final String labelKey ) {
- final MenuItem menuItem = createMenuItem( labelKey );
- items.add( menuItem );
- return menuItem;
- }
-
- private MenuItem createMenuItem( final String labelKey ) {
- return new MenuItem( get( labelKey ) );
- }
-
- private VariableTreeItem<String> createTreeItem() {
- return new VariableTreeItem<>( get( "Definition.menu.add.default" ) );
- }
-
- private TreeCell<String> createTreeCell() {
- return new TextFieldTreeCell<>(
- createStringConverter() ) {
- @Override
- public void commitEdit( final String newValue ) {
- super.commitEdit( newValue );
- select( getTreeItem() );
- requestFocus();
- }
- };
- }
-
- @Override
- public void requestFocus() {
- super.requestFocus();
- getTreeView().requestFocus();
- }
-
- private StringConverter<String> createStringConverter() {
- return new StringConverter<>() {
- @Override
- public String toString( final String object ) {
- return object == null ? "" : object;
- }
-
- @Override
- public String fromString( final String string ) {
- return string == null ? "" : string;
- }
- };
- }
-
- /**
- * Returns the tree view that contains the definition hierarchy.
- *
- * @return A non-null instance.
- */
- public TreeView<String> getTreeView() {
- return mTreeView;
- }
-
- /**
- * Returns the root node to the tree view.
- *
- * @return getTreeView()
- */
- public Node getNode() {
- return getTreeView();
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.*;
+import javafx.scene.control.cell.TextFieldTreeCell;
+import javafx.scene.input.KeyEvent;
+import javafx.util.StringConverter;
+
+import java.util.*;
+
+import static com.scrivenvar.Messages.get;
+import static javafx.scene.input.KeyEvent.KEY_PRESSED;
+
+/**
+ * Provides the user interface that holdsa {@link TreeView}, which
+ * allows users to interact with key/value pairs loaded from the
+ * {@link DocumentParser} and adapted using a {@link TreeAdapter}.
+ *
+ * @author White Magic Software, Ltd.
+ */
+public final class DefinitionPane extends TitledPane {
+
+ /**
+ * Trimmed off the end of a word to match a variable name.
+ */
+ private final static String TERMINALS = ":;,.!?-/\\¡¿";
+
+ /**
+ * Contains a view of the definitions.
+ */
+ private final TreeView<String> mTreeView = new TreeView<>();
+
+ /**
+ * Handlers for key press events.
+ */
+ private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
+ = new HashSet<>();
+
+ /**
+ * Definition file name shown in the title of the pane.
+ */
+ private final StringProperty mFilename = new SimpleStringProperty();
+
+ /**
+ * Constructs a definition pane with a given tree view root.
+ */
+ public DefinitionPane() {
+ final var treeView = getTreeView();
+ treeView.setEditable( true );
+ treeView.setCellFactory( cell -> createTreeCell() );
+ treeView.setContextMenu( createContextMenu() );
+ treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
+ treeView.setShowRoot( false );
+ getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
+
+ textProperty().bind( mFilename );
+
+ setContent( treeView );
+ setCollapsible( false );
+ }
+
+ /**
+ * Changes the root of the {@link TreeView} to the root of the
+ * {@link TreeView} from the {@link DefinitionSource}.
+ *
+ * @param definitionSource Container for the hierarchy of key/value pairs
+ * to replace the existing hierarchy.
+ */
+ public void update( final DefinitionSource definitionSource ) {
+ assert definitionSource != null;
+
+ final TreeAdapter treeAdapter = definitionSource.getTreeAdapter();
+ final TreeItem<String> root = treeAdapter.adapt(
+ get( "Pane.definition.node.root.title" )
+ );
+
+ getTreeView().setRoot( root );
+ }
+
+ public Map<String, String> toMap() {
+ return TreeItemAdapter.toMap( getTreeView().getRoot() );
+ }
+
+ /**
+ * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
+ * is modified. The modifications include: item value changes, item additions,
+ * and item removals.
+ * <p>
+ * Safe to call multiple times; if a handler is already registered, the
+ * old handler is used.
+ * </p>
+ *
+ * @param handler The handler to call whenever any {@link TreeItem} changes.
+ */
+ public void addTreeChangeHandler(
+ final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
+ final TreeItem<String> root = getTreeView().getRoot();
+ root.addEventHandler( TreeItem.valueChangedEvent(), handler );
+ root.addEventHandler( TreeItem.childrenModificationEvent(), handler );
+ }
+
+ public void addKeyEventHandler(
+ final EventHandler<? super KeyEvent> handler ) {
+ getKeyEventHandlers().add( handler );
+ }
+
+ /**
+ * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
+ * well-formed for export. A tree is considered well-formed if the following
+ * conditions are met:
+ *
+ * <ul>
+ * <li>The root node contains at least one child node having a leaf.</li>
+ * <li>There are no leaf nodes with sibling leaf nodes.</li>
+ * </ul>
+ *
+ * @return {@code null} if the document is well-formed, otherwise the
+ * problematic child {@link TreeItem}.
+ */
+ public TreeItem<String> isTreeWellFormed() {
+ final var root = getTreeView().getRoot();
+
+ for( final var child : root.getChildren() ) {
+ final var problemChild = isWellFormed( child );
+
+ if( child.isLeaf() || problemChild != null ) {
+ return problemChild;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Determines whether the document is well-formed by ensuring that
+ * child branches do not contain multiple leaves.
+ *
+ * @param item The sub-tree to check for well-formedness.
+ * @return {@code null} when the tree is well-formed, otherwise the
+ * problematic {@link TreeItem}.
+ */
+ private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
+ int childLeafs = 0;
+ int childBranches = 0;
+
+ for( final TreeItem<String> child : item.getChildren() ) {
+ if( child.isLeaf() ) {
+ childLeafs++;
+ }
+ else {
+ childBranches++;
+ }
+
+ final var problemChild = isWellFormed( child );
+
+ if( problemChild != null ) {
+ return problemChild;
+ }
+ }
+
+ return ((childBranches > 0 && childLeafs == 0) ||
+ (childBranches == 0 && childLeafs <= 1)) ? null : item;
+ }
+
+ /**
+ * Returns the leaf that matches the given value. If the value is terminally
+ * punctuated, the punctuation is removed if no match was found.
+ *
+ * @param value The value to find, never null.
+ * @param findMode Defines how to match words.
+ * @return The leaf that contains the given value, or null if neither the
+ * original value nor the terminally-trimmed value was found.
+ */
+ public VariableTreeItem<String> findLeaf(
+ final String value, final FindMode findMode ) {
+ final VariableTreeItem<String> root = getTreeRoot();
+ final VariableTreeItem<String> leaf = root.findLeaf( value, findMode );
+
+ return leaf == null
+ ? root.findLeaf( rtrimTerminalPunctuation( value ) )
+ : leaf;
+ }
+
+ /**
+ * Removes punctuation from the end of a string.
+ *
+ * @param s The string to trim, never null.
+ * @return The string trimmed of all terminal characters from the end
+ */
+ private String rtrimTerminalPunctuation( final String s ) {
+ assert s != null;
+ int index = s.length() - 1;
+
+ while( index > 0 && (TERMINALS.indexOf( s.charAt( index ) ) >= 0) ) {
+ index--;
+ }
+
+ return s.substring( 0, index );
+ }
+
+ /**
+ * Expands the node to the root, recursively.
+ *
+ * @param <T> The type of tree item to expand (usually String).
+ * @param node The node to expand.
+ */
+ public <T> void expand( final TreeItem<T> node ) {
+ if( node != null ) {
+ expand( node.getParent() );
+
+ if( !node.isLeaf() ) {
+ node.setExpanded( true );
+ }
+ }
+ }
+
+ public void select( final TreeItem<String> item ) {
+ getSelectionModel().clearSelection();
+ getSelectionModel().select( getTreeView().getRow( item ) );
+ }
+
+ /**
+ * Collapses the tree, recursively.
+ */
+ public void collapse() {
+ collapse( getTreeRoot().getChildren() );
+ }
+
+ /**
+ * Collapses the tree, recursively.
+ *
+ * @param <T> The type of tree item to expand (usually String).
+ * @param nodes The nodes to collapse.
+ */
+ private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
+ for( final TreeItem<T> node : nodes ) {
+ node.setExpanded( false );
+ collapse( node.getChildren() );
+ }
+ }
+
+ /**
+ * @return {@code true} when the user is editing a {@link TreeItem}.
+ */
+ private boolean isEditingTreeItem() {
+ return getTreeView().editingItemProperty().getValue() != null;
+ }
+
+ /**
+ * Changes to edit mode for the selected item.
+ */
+ private void editSelectedItem() {
+ getTreeView().edit( getSelectedItem() );
+ }
+
+ /**
+ * Removes all selected items from the {@link TreeView}.
+ */
+ private void deleteSelectedItems() {
+ for( final TreeItem<String> item : getSelectedItems() ) {
+ final TreeItem<String> parent = item.getParent();
+
+ if( parent != null ) {
+ parent.getChildren().remove( item );
+ }
+ }
+ }
+
+ /**
+ * Deletes the selected item.
+ */
+ private void deleteSelectedItem() {
+ final TreeItem<String> c = getSelectedItem();
+ getSiblings( c ).remove( c );
+ }
+
+ /**
+ * Adds a new item under the selected item (or root if nothing is selected).
+ * There are a few conditions to consider: when adding to the root,
+ * when adding to a leaf, and when adding to a non-leaf. Items added to the
+ * root must contain two items: a key and a value.
+ */
+ private void addItem() {
+ final TreeItem<String> value = createTreeItem();
+ getSelectedItem().getChildren().add( value );
+ expand( value );
+ select( value );
+ }
+
+ private ContextMenu createContextMenu() {
+ final ContextMenu menu = new ContextMenu();
+ final ObservableList<MenuItem> items = menu.getItems();
+
+ addMenuItem( items, "Definition.menu.create" )
+ .setOnAction( e -> addItem() );
+
+ addMenuItem( items, "Definition.menu.rename" )
+ .setOnAction( e -> editSelectedItem() );
+
+ addMenuItem( items, "Definition.menu.remove" )
+ .setOnAction( e -> deleteSelectedItem() );
+
+ return menu;
+ }
+
+ /**
+ * Executes hot-keys for edits to the definition tree.
+ *
+ * @param event Contains the key code of the key that was pressed.
+ */
+ private void keyEventFilter( final KeyEvent event ) {
+ if( !isEditingTreeItem() ) {
+ switch( event.getCode() ) {
+ case ENTER:
+ expand( getSelectedItem() );
+ event.consume();
+ break;
+
+ case DELETE:
+ deleteSelectedItems();
+ break;
+
+ case INSERT:
+ addItem();
+ break;
+
+ case R:
+ if( event.isControlDown() ) {
+ editSelectedItem();
+ }
+
+ break;
+ }
+
+ for( final var handler : getKeyEventHandlers() ) {
+ handler.handle( event );
+ }
+ }
+ }
+
+ /**
+ * Adds a menu item to a list of menu items.
+ *
+ * @param items The list of menu items to append to.
+ * @param labelKey The resource bundle key name for the menu item's label.
+ * @return The menu item added to the list of menu items.
+ */
+ private MenuItem addMenuItem(
+ final List<MenuItem> items, final String labelKey ) {
+ final MenuItem menuItem = createMenuItem( labelKey );
+ items.add( menuItem );
+ return menuItem;
+ }
+
+ private MenuItem createMenuItem( final String labelKey ) {
+ return new MenuItem( get( labelKey ) );
+ }
+
+ private VariableTreeItem<String> createTreeItem() {
+ return new VariableTreeItem<>( get( "Definition.menu.add.default" ) );
+ }
+
+ private TreeCell<String> createTreeCell() {
+ return new TextFieldTreeCell<>(
+ createStringConverter() ) {
+ @Override
+ public void commitEdit( final String newValue ) {
+ super.commitEdit( newValue );
+ select( getTreeItem() );
+ requestFocus();
+ }
+ };
+ }
+
+ @Override
+ public void requestFocus() {
+ super.requestFocus();
+ getTreeView().requestFocus();
+ }
+
+ private StringConverter<String> createStringConverter() {
+ return new StringConverter<>() {
+ @Override
+ public String toString( final String object ) {
+ return object == null ? "" : object;
+ }
+
+ @Override
+ public String fromString( final String string ) {
+ return string == null ? "" : string;
+ }
+ };
+ }
+
+ /**
+ * Returns the tree view that contains the definition hierarchy.
+ *
+ * @return A non-null instance.
+ */
+ public TreeView<String> getTreeView() {
+ return mTreeView;
+ }
+
+ /**
+ * Returns this pane.
+ *
+ * @return this
+ */
+ public Node getNode() {
+ return this;
+ }
+
+ /**
+ * Returns the property used to set the title of the pane: the file name.
+ *
+ * @return A non-null property used for showing the definition file name.
+ */
+ public StringProperty filenameProperty() {
+ return mFilename;
}
src/main/java/com/scrivenvar/MainWindow.java
import javafx.stage.Window;
import javafx.stage.WindowEvent;
-import org.controlsfx.control.StatusBar;
-import org.fxmisc.richtext.model.TwoDimensional.Position;
-
-import java.io.File;
-import java.nio.file.Path;
-import java.util.*;
-import java.util.function.Function;
-import java.util.prefs.Preferences;
-
-import static com.scrivenvar.Constants.*;
-import static com.scrivenvar.Messages.get;
-import static com.scrivenvar.Messages.getLiteral;
-import static com.scrivenvar.util.StageState.*;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
-import static javafx.event.Event.fireEvent;
-import static javafx.scene.input.KeyCode.ENTER;
-import static javafx.scene.input.KeyCode.TAB;
-import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
-
-/**
- * Main window containing a tab pane in the center for file editors.
- *
- * @author Karl Tauber and White Magic Software, Ltd.
- */
-public class MainWindow implements Observer {
-
- private final Options mOptions = Services.load( Options.class );
- private final Snitch mSnitch = Services.load( Snitch.class );
- private final Settings mSettings = Services.load( Settings.class );
- private final Notifier mNotifier = Services.load( Notifier.class );
-
- private final Scene mScene;
- private final StatusBar mStatusBar;
- private final Text mLineNumberText;
- private final TextField mFindTextField;
-
- private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
- private final DefinitionPane mDefinitionPane = new DefinitionPane();
- private final HTMLPreviewPane mPreviewPane = new HTMLPreviewPane();
- private FileEditorTabPane fileEditorPane;
-
- /**
- * Prevents re-instantiation of processing classes.
- */
- private final Map<FileEditorTab, Processor<String>> mProcessors =
- new HashMap<>();
-
- private final Map<String, String> mResolvedMap =
- new HashMap<>( DEFAULT_MAP_SIZE );
-
- /**
- * Listens on the definition pane for double-click events.
- */
- private VariableNameInjector variableNameInjector;
-
- /**
- * Called when the definition data is changed.
- */
- final EventHandler<TreeItem.TreeModificationEvent<Event>> mTreeHandler =
- event -> {
- exportDefinitions( getDefinitionPath() );
- interpolateResolvedMap();
- refreshActiveTab();
- };
-
- final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
- event -> {
- if( event.getCode() == ENTER ) {
- getVariableNameInjector().injectSelectedItem();
- }
- };
-
- final EventHandler<? super KeyEvent> mEditorKeyHandler =
- (EventHandler<KeyEvent>) event -> {
- if( event.getCode() == TAB ) {
- getDefinitionPane().requestFocus();
- event.consume();
- }
- };
-
- public MainWindow() {
- mStatusBar = createStatusBar();
- mLineNumberText = createLineNumberText();
- mFindTextField = createFindTextField();
- mScene = createScene();
-
- initLayout();
- initFindInput();
- initSnitch();
- initDefinitionListener();
- initTabAddedListener();
- initTabChangedListener();
- initPreferences();
- }
-
- /**
- * Watch for changes to external files. In particular, this awaits
- * modifications to any XSL files associated with XML files being edited. When
- * an XSL file is modified (external to the application), the snitch's ears
- * perk up and the file is reloaded. This keeps the XSL transformation up to
- * date with what's on the file system.
- */
- private void initSnitch() {
- getSnitch().addObserver( this );
- }
-
- /**
- * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
- * presses.
- */
- private void initFindInput() {
- final TextField input = getFindTextField();
-
- input.setOnKeyPressed( ( KeyEvent event ) -> {
- switch( event.getCode() ) {
- case F3:
- case ENTER:
- findNext();
- break;
- case F:
- if( !event.isControlDown() ) {
- break;
- }
- case ESCAPE:
- getStatusBar().setGraphic( null );
- getActiveFileEditor().getEditorPane().requestFocus();
- break;
- }
- } );
-
- // Remove when the input field loses focus.
- input.focusedProperty().addListener(
- (
- final ObservableValue<? extends Boolean> focused,
- final Boolean oFocus,
- final Boolean nFocus ) -> {
- if( !nFocus ) {
- getStatusBar().setGraphic( null );
- }
- }
- );
- }
-
- /**
- * Listen for {@link FileEditorTabPane} to receive open definition file event.
- */
- private void initDefinitionListener() {
- getFileEditorPane().onOpenDefinitionFileProperty().addListener(
- ( final ObservableValue<? extends Path> file,
- final Path oldPath, final Path newPath ) -> {
- // Indirectly refresh the resolved map.
- resetProcessors();
-
- openDefinitions( newPath );
-
- // Will create new processors and therefore a new resolved map.
- refreshActiveTab();
- }
- );
- }
-
- /**
- * When tabs are added, hook the various change listeners onto the new tab so
- * that the preview pane refreshes as necessary.
- */
- 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 );
- initKeyboardEventListeners( tab );
-// initSyntaxListener( tab );
- }
- }
- }
- }
- );
- }
-
- /**
- * Reloads the preferences from the previous session.
- */
- private void initPreferences() {
- restoreDefinitionPane();
- getFileEditorPane().restorePreferences();
- }
-
- /**
- * Listen for new tab selection events.
- */
- private void initTabChangedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Update the preview pane changing tabs.
- editorPane.addTabSelectionListener(
- ( ObservableValue<? extends Tab> tabPane,
- final Tab oldTab, final Tab newTab ) -> {
- updateVariableNameInjector();
-
- // If there was no old tab, then this is a first time load, which
- // can be ignored.
- if( oldTab != null ) {
- if( newTab == null ) {
- closeRemainingTab();
- }
- else {
- // Update the preview with the edited text.
- refreshSelectedTab( (FileEditorTab) newTab );
- }
- }
- }
- );
- }
-
- /**
- * Ensure that the keyboard events are received when a new tab is added
- * to the user interface.
- *
- * @param tab The tab that can trigger keyboard events, such as control+space.
- */
- private void initKeyboardEventListeners( final FileEditorTab tab ) {
- final VariableNameInjector vin = getVariableNameInjector();
- vin.initKeyboardEventListeners( tab );
-
- tab.addEventFilter( KeyEvent.KEY_PRESSED, mEditorKeyHandler );
- }
-
- private void initTextChangeListener( final FileEditorTab tab ) {
- tab.addTextChangeListener(
- ( ObservableValue<? extends String> editor,
- final String oldValue, final String newValue ) ->
- refreshSelectedTab( tab )
- );
- }
-
- private void initCaretParagraphListener( final FileEditorTab tab ) {
- tab.addCaretParagraphListener(
- ( ObservableValue<? extends Integer> editor,
- final Integer oldValue, final Integer newValue ) ->
- refreshSelectedTab( tab )
- );
- }
-
- private void updateVariableNameInjector() {
- getVariableNameInjector().setFileEditorTab( getActiveFileEditor() );
- }
-
- private void setVariableNameInjector( final VariableNameInjector injector ) {
- this.variableNameInjector = injector;
- }
-
- private synchronized VariableNameInjector getVariableNameInjector() {
- if( this.variableNameInjector == null ) {
- final VariableNameInjector vin = createVariableNameInjector();
- setVariableNameInjector( vin );
- }
-
- return this.variableNameInjector;
- }
-
- private VariableNameInjector createVariableNameInjector() {
- final FileEditorTab tab = getActiveFileEditor();
- final DefinitionPane pane = getDefinitionPane();
-
- return new VariableNameInjector( tab, pane );
- }
-
- /**
- * 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 refreshSelectedTab( final FileEditorTab tab ) {
- if( tab == null ) {
- return;
- }
-
- getPreviewPane().setPath( tab.getPath() );
-
- // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
- final Position p = tab.getCaretOffset();
- getLineNumberText().setText(
- get( STATUS_BAR_LINE,
- p.getMajor() + 1,
- p.getMinor() + 1,
- tab.getCaretPosition() + 1
- )
- );
-
- Processor<String> processor = getProcessors().get( tab );
-
- if( processor == null ) {
- processor = createProcessor( tab );
- getProcessors().put( tab, processor );
- }
-
- try {
- processor.processChain( tab.getEditorText() );
- } catch( final Exception ex ) {
- error( ex );
- }
- }
-
- private void refreshActiveTab() {
- refreshSelectedTab( getActiveFileEditor() );
- }
-
- /**
- * Used to find text in the active file editor window.
- */
- private void find() {
- final TextField input = getFindTextField();
- getStatusBar().setGraphic( input );
- input.requestFocus();
- }
-
- public void findNext() {
- getActiveFileEditor().searchNext( getFindTextField().getText() );
- }
-
- /**
- * Returns the variable map of interpolated definitions.
- *
- * @return A map to help dereference variables.
- */
- private Map<String, String> getResolvedMap() {
- return mResolvedMap;
- }
-
- private void interpolateResolvedMap() {
- final Map<String, String> treeMap = getDefinitionPane().toMap();
- final Map<String, String> map = new HashMap<>( treeMap );
- MapInterpolator.interpolate( map );
-
- getResolvedMap().clear();
- getResolvedMap().putAll( map );
- }
-
- /**
- * Called when a definition source is opened.
- *
- * @param path Path to the definition source that was opened.
- */
- private void openDefinitions( final Path path ) {
- try {
- final DefinitionSource ds = createDefinitionSource( path );
- setDefinitionSource( ds );
- storeDefinitionSourceFilename( path );
-
- final DefinitionPane pane = getDefinitionPane();
- pane.update( ds );
- pane.addTreeChangeHandler( mTreeHandler );
- pane.addKeyEventHandler( mDefinitionKeyHandler );
-
- interpolateResolvedMap();
- } catch( final Exception e ) {
- error( e );
- }
- }
-
- private void exportDefinitions( final Path path ) {
- try {
- final DefinitionPane pane = getDefinitionPane();
- final TreeItem<String> root = pane.getTreeView().getRoot();
- final TreeItem<String> problemChild = pane.isTreeWellFormed();
-
- if( problemChild == null ) {
- getDefinitionSource().getTreeAdapter().export( root, path );
- getNotifier().clear();
- }
- else {
- final String msg = get( "yaml.error.tree.form",
- problemChild.getValue() );
- getNotifier().notify( msg );
- }
- } catch( final Exception e ) {
- error( e );
- }
- }
-
- private Path getDefinitionPath() {
- final String source = getPreferences().get(
- PERSIST_DEFINITION_SOURCE, "" );
-
- return new File(
- source.isBlank()
- ? getSetting( "file.definition.default", "variables.yaml" )
- : source
- ).toPath();
- }
-
- private void restoreDefinitionPane() {
- openDefinitions( getDefinitionPath() );
- }
-
- private void storeDefinitionSourceFilename( final Path path ) {
- getPreferences().put( PERSIST_DEFINITION_SOURCE, path.toString() );
- }
-
- /**
- * Called when the last open tab is closed to clear the preview pane.
- */
- private void closeRemainingTab() {
- getPreviewPane().clear();
- }
-
- /**
- * Called when an exception occurs that warrants the user's attention.
- *
- * @param e The exception with a message that the user should know about.
- */
- private void error( final Exception e ) {
- getNotifier().notify( e );
- }
-
- //---- File actions -------------------------------------------------------
-
- /**
- * Called when an observable instance has changed. This is called by both the
- * snitch service and the notify service. The snitch service can be called for
- * different file types, including definition sources.
- *
- * @param observable The observed instance.
- * @param value The noteworthy item.
- */
- @Override
- public void update( final Observable observable, final Object value ) {
- if( value != null ) {
- if( observable instanceof Snitch && value instanceof Path ) {
- updateSelectedTab();
- }
- else if( observable instanceof Notifier && value instanceof String ) {
- updateStatusBar( (String) value );
- }
- }
- }
-
- /**
- * Updates the status bar to show the given message.
- *
- * @param s The message to show in the status bar.
- */
- private void updateStatusBar( final String s ) {
- Platform.runLater(
- () -> {
- final int index = s.indexOf( '\n' );
- final String message = s.substring(
- 0, index > 0 ? index : s.length() );
-
- getStatusBar().setText( message );
- }
- );
- }
-
- /**
- * Called when a file has been modified.
- */
- private void updateSelectedTab() {
- Platform.runLater(
- () -> {
- // Brute-force XSLT file reload by re-instantiating all processors.
- resetProcessors();
- refreshActiveTab();
- }
- );
- }
-
- /**
- * After resetting the processors, they will refresh anew to be up-to-date
- * with the files (text and definition) currently loaded into the editor.
- */
- private void resetProcessors() {
- getProcessors().clear();
- }
-
- //---- 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 fileSaveAs() {
- final FileEditorTab editor = getActiveFileEditor();
- getFileEditorPane().saveEditorAs( editor );
- getProcessors().remove( editor );
-
- try {
- refreshSelectedTab( editor );
- } catch( final Exception ex ) {
- getNotifier().notify( ex );
- }
- }
-
- private void fileSaveAll() {
- getFileEditorPane().saveAllEditors();
- }
-
- private void fileExit() {
- final Window window = getWindow();
- fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
- }
-
- //---- R menu actions
- private void rScript() {
- final String script = getPreferences().get( PERSIST_R_STARTUP, "" );
- final RScriptDialog dialog = new RScriptDialog(
- getWindow(), "Dialog.r.script.title", script );
- final Optional<String> result = dialog.showAndWait();
-
- result.ifPresent( this::putStartupScript );
- }
-
- private void rDirectory() {
- final TextInputDialog dialog = new TextInputDialog(
- getPreferences().get( PERSIST_R_DIRECTORY, USER_DIRECTORY )
- );
-
- dialog.setTitle( get( "Dialog.r.directory.title" ) );
- dialog.setHeaderText( getLiteral( "Dialog.r.directory.header" ) );
- dialog.setContentText( "Directory" );
-
- final Optional<String> result = dialog.showAndWait();
-
- result.ifPresent( this::putStartupDirectory );
- }
-
- /**
- * Stores the R startup script into the user preferences.
- */
- private void putStartupScript( final String script ) {
- putPreference( PERSIST_R_STARTUP, script );
- }
-
- /**
- * Stores the R bootstrap script directory into the user preferences.
- */
- private void putStartupDirectory( final String directory ) {
- putPreference( PERSIST_R_DIRECTORY, directory );
- }
-
- //---- Help actions -------------------------------------------------------
- private void helpAbout() {
- final Alert alert = new Alert( AlertType.INFORMATION );
- alert.setTitle( get( "Dialog.about.title" ) );
- alert.setHeaderText( get( "Dialog.about.header" ) );
- alert.setContentText( get( "Dialog.about.content" ) );
- alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
- alert.initOwner( getWindow() );
-
- alert.showAndWait();
- }
-
- //---- Convenience accessors ----------------------------------------------
- private float getFloat( final String key, final float defaultValue ) {
- return getPreferences().getFloat( key, defaultValue );
- }
-
- private Preferences getPreferences() {
- return getOptions().getState();
- }
-
- protected Scene getScene() {
- return mScene;
- }
-
- public Window getWindow() {
- return getScene().getWindow();
- }
-
- private MarkdownEditorPane getActiveEditor() {
- final EditorPane pane = getActiveFileEditor().getEditorPane();
-
- return pane instanceof MarkdownEditorPane
- ? (MarkdownEditorPane) pane
- : null;
- }
-
- private FileEditorTab getActiveFileEditor() {
- return getFileEditorPane().getActiveFileEditor();
- }
-
- //---- Member accessors ---------------------------------------------------
-
- private Map<FileEditorTab, Processor<String>> getProcessors() {
- return mProcessors;
- }
-
- private FileEditorTabPane getFileEditorPane() {
- if( this.fileEditorPane == null ) {
- this.fileEditorPane = createFileEditorPane();
- }
-
- return this.fileEditorPane;
- }
-
- private HTMLPreviewPane getPreviewPane() {
- return mPreviewPane;
- }
-
- private void setDefinitionSource( final DefinitionSource definitionSource ) {
- assert definitionSource != null;
- mDefinitionSource = definitionSource;
- }
-
- private DefinitionSource getDefinitionSource() {
- return mDefinitionSource;
- }
-
- private DefinitionPane getDefinitionPane() {
- return mDefinitionPane;
- }
-
- private Options getOptions() {
- return mOptions;
- }
-
- private Snitch getSnitch() {
- return mSnitch;
- }
-
- private Notifier getNotifier() {
- return mNotifier;
- }
-
- private Text getLineNumberText() {
- return mLineNumberText;
- }
-
- private StatusBar getStatusBar() {
- return mStatusBar;
- }
-
- private TextField getFindTextField() {
- return mFindTextField;
- }
-
- //---- Member creators ----------------------------------------------------
-
- /**
- * Factory to create processors that are suited to different file types.
- *
- * @param tab The tab that is subjected to processing.
- * @return A processor suited to the file type specified by the tab's path.
- */
- private Processor<String> createProcessor( final FileEditorTab tab ) {
- return createProcessorFactory().createProcessor( tab );
- }
-
- private ProcessorFactory createProcessorFactory() {
- return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
- }
-
- private DefinitionSource createDefaultDefinitionSource() {
- return new YamlDefinitionSource( getDefinitionPath() );
- }
-
- private DefinitionSource createDefinitionSource( final Path path ) {
- try {
- return createDefinitionFactory().createDefinitionSource( path );
- } catch( final Exception ex ) {
- error( ex );
- return createDefaultDefinitionSource();
- }
- }
-
- private TextField createFindTextField() {
- return new TextField();
- }
-
- /**
- * Create an editor pane to hold file editor tabs.
- *
- * @return A new instance, never null.
- */
- private FileEditorTabPane createFileEditorPane() {
- return new FileEditorTabPane();
- }
-
- private DefinitionFactory createDefinitionFactory() {
- return new DefinitionFactory();
- }
-
- private StatusBar createStatusBar() {
- return new StatusBar();
- }
-
- private Scene createScene() {
- 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.setBottom( getStatusBar() );
- borderPane.setCenter( splitPane );
-
- final VBox box = new VBox();
- box.setAlignment( Pos.BASELINE_CENTER );
- box.getChildren().add( getLineNumberText() );
- getStatusBar().getRightItems().add( box );
+import javafx.util.Duration;
+import org.controlsfx.control.StatusBar;
+import org.fxmisc.richtext.model.TwoDimensional.Position;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.function.Function;
+import java.util.prefs.Preferences;
+
+import static com.scrivenvar.Constants.*;
+import static com.scrivenvar.Messages.get;
+import static com.scrivenvar.Messages.getLiteral;
+import static com.scrivenvar.util.StageState.*;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
+import static javafx.event.Event.fireEvent;
+import static javafx.scene.input.KeyCode.ENTER;
+import static javafx.scene.input.KeyCode.TAB;
+import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
+
+/**
+ * Main window containing a tab pane in the center for file editors.
+ *
+ * @author Karl Tauber and White Magic Software, Ltd.
+ */
+public class MainWindow implements Observer {
+
+ private final Options mOptions = Services.load( Options.class );
+ private final Snitch mSnitch = Services.load( Snitch.class );
+ private final Settings mSettings = Services.load( Settings.class );
+ private final Notifier mNotifier = Services.load( Notifier.class );
+
+ private final Scene mScene;
+ private final StatusBar mStatusBar;
+ private final Text mLineNumberText;
+ private final TextField mFindTextField;
+
+ private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
+ private final DefinitionPane mDefinitionPane = new DefinitionPane();
+ private final HTMLPreviewPane mPreviewPane = new HTMLPreviewPane();
+ private FileEditorTabPane fileEditorPane;
+
+ /**
+ * Prevents re-instantiation of processing classes.
+ */
+ private final Map<FileEditorTab, Processor<String>> mProcessors =
+ new HashMap<>();
+
+ private final Map<String, String> mResolvedMap =
+ new HashMap<>( DEFAULT_MAP_SIZE );
+
+ /**
+ * Listens on the definition pane for double-click events.
+ */
+ private VariableNameInjector variableNameInjector;
+
+ /**
+ * Called when the definition data is changed.
+ */
+ final EventHandler<TreeItem.TreeModificationEvent<Event>> mTreeHandler =
+ event -> {
+ exportDefinitions( getDefinitionPath() );
+ interpolateResolvedMap();
+ refreshActiveTab();
+ };
+
+ final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
+ event -> {
+ if( event.getCode() == ENTER ) {
+ getVariableNameInjector().injectSelectedItem();
+ }
+ };
+
+ final EventHandler<? super KeyEvent> mEditorKeyHandler =
+ (EventHandler<KeyEvent>) event -> {
+ if( event.getCode() == TAB ) {
+ getDefinitionPane().requestFocus();
+ event.consume();
+ }
+ };
+
+ public MainWindow() {
+ mStatusBar = createStatusBar();
+ mLineNumberText = createLineNumberText();
+ mFindTextField = createFindTextField();
+ mScene = createScene();
+
+ initLayout();
+ initFindInput();
+ initSnitch();
+ initDefinitionListener();
+ initTabAddedListener();
+ initTabChangedListener();
+ initPreferences();
+ }
+
+ /**
+ * Watch for changes to external files. In particular, this awaits
+ * modifications to any XSL files associated with XML files being edited. When
+ * an XSL file is modified (external to the application), the snitch's ears
+ * perk up and the file is reloaded. This keeps the XSL transformation up to
+ * date with what's on the file system.
+ */
+ private void initSnitch() {
+ getSnitch().addObserver( this );
+ }
+
+ /**
+ * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
+ * presses.
+ */
+ private void initFindInput() {
+ final TextField input = getFindTextField();
+
+ input.setOnKeyPressed( ( KeyEvent event ) -> {
+ switch( event.getCode() ) {
+ case F3:
+ case ENTER:
+ findNext();
+ break;
+ case F:
+ if( !event.isControlDown() ) {
+ break;
+ }
+ case ESCAPE:
+ getStatusBar().setGraphic( null );
+ getActiveFileEditor().getEditorPane().requestFocus();
+ break;
+ }
+ } );
+
+ // Remove when the input field loses focus.
+ input.focusedProperty().addListener(
+ (
+ final ObservableValue<? extends Boolean> focused,
+ final Boolean oFocus,
+ final Boolean nFocus ) -> {
+ if( !nFocus ) {
+ getStatusBar().setGraphic( null );
+ }
+ }
+ );
+ }
+
+ /**
+ * Listen for {@link FileEditorTabPane} to receive open definition file event.
+ */
+ private void initDefinitionListener() {
+ getFileEditorPane().onOpenDefinitionFileProperty().addListener(
+ ( final ObservableValue<? extends Path> file,
+ final Path oldPath, final Path newPath ) -> {
+ // Indirectly refresh the resolved map.
+ resetProcessors();
+
+ openDefinitions( newPath );
+
+ // Will create new processors and therefore a new resolved map.
+ refreshActiveTab();
+ }
+ );
+ }
+
+ /**
+ * When tabs are added, hook the various change listeners onto the new tab so
+ * that the preview pane refreshes as necessary.
+ */
+ 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 );
+ initKeyboardEventListeners( tab );
+// initSyntaxListener( tab );
+ }
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * Reloads the preferences from the previous session.
+ */
+ private void initPreferences() {
+ restoreDefinitionPane();
+ getFileEditorPane().restorePreferences();
+ }
+
+ /**
+ * Listen for new tab selection events.
+ */
+ private void initTabChangedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Update the preview pane changing tabs.
+ editorPane.addTabSelectionListener(
+ ( ObservableValue<? extends Tab> tabPane,
+ final Tab oldTab, final Tab newTab ) -> {
+ updateVariableNameInjector();
+
+ // If there was no old tab, then this is a first time load, which
+ // can be ignored.
+ if( oldTab != null ) {
+ if( newTab == null ) {
+ closeRemainingTab();
+ }
+ else {
+ // Update the preview with the edited text.
+ refreshSelectedTab( (FileEditorTab) newTab );
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * Ensure that the keyboard events are received when a new tab is added
+ * to the user interface.
+ *
+ * @param tab The tab that can trigger keyboard events, such as control+space.
+ */
+ private void initKeyboardEventListeners( final FileEditorTab tab ) {
+ final VariableNameInjector vin = getVariableNameInjector();
+ vin.initKeyboardEventListeners( tab );
+
+ tab.addEventFilter( KeyEvent.KEY_PRESSED, mEditorKeyHandler );
+ }
+
+ private void initTextChangeListener( final FileEditorTab tab ) {
+ tab.addTextChangeListener(
+ ( ObservableValue<? extends String> editor,
+ final String oldValue, final String newValue ) ->
+ refreshSelectedTab( tab )
+ );
+ }
+
+ private void initCaretParagraphListener( final FileEditorTab tab ) {
+ tab.addCaretParagraphListener(
+ ( ObservableValue<? extends Integer> editor,
+ final Integer oldValue, final Integer newValue ) ->
+ refreshSelectedTab( tab )
+ );
+ }
+
+ private void updateVariableNameInjector() {
+ getVariableNameInjector().setFileEditorTab( getActiveFileEditor() );
+ }
+
+ private void setVariableNameInjector( final VariableNameInjector injector ) {
+ this.variableNameInjector = injector;
+ }
+
+ private synchronized VariableNameInjector getVariableNameInjector() {
+ if( this.variableNameInjector == null ) {
+ final VariableNameInjector vin = createVariableNameInjector();
+ setVariableNameInjector( vin );
+ }
+
+ return this.variableNameInjector;
+ }
+
+ private VariableNameInjector createVariableNameInjector() {
+ final FileEditorTab tab = getActiveFileEditor();
+ final DefinitionPane pane = getDefinitionPane();
+
+ return new VariableNameInjector( tab, pane );
+ }
+
+ /**
+ * 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 refreshSelectedTab( final FileEditorTab tab ) {
+ if( tab == null ) {
+ return;
+ }
+
+ getPreviewPane().setPath( tab.getPath() );
+
+ // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
+ final Position p = tab.getCaretOffset();
+ getLineNumberText().setText(
+ get( STATUS_BAR_LINE,
+ p.getMajor() + 1,
+ p.getMinor() + 1,
+ tab.getCaretPosition() + 1
+ )
+ );
+
+ Processor<String> processor = getProcessors().get( tab );
+
+ if( processor == null ) {
+ processor = createProcessor( tab );
+ getProcessors().put( tab, processor );
+ }
+
+ try {
+ processor.processChain( tab.getEditorText() );
+ } catch( final Exception ex ) {
+ error( ex );
+ }
+ }
+
+ private void refreshActiveTab() {
+ refreshSelectedTab( getActiveFileEditor() );
+ }
+
+ /**
+ * Used to find text in the active file editor window.
+ */
+ private void find() {
+ final TextField input = getFindTextField();
+ getStatusBar().setGraphic( input );
+ input.requestFocus();
+ }
+
+ public void findNext() {
+ getActiveFileEditor().searchNext( getFindTextField().getText() );
+ }
+
+ /**
+ * Returns the variable map of interpolated definitions.
+ *
+ * @return A map to help dereference variables.
+ */
+ private Map<String, String> getResolvedMap() {
+ return mResolvedMap;
+ }
+
+ private void interpolateResolvedMap() {
+ final Map<String, String> treeMap = getDefinitionPane().toMap();
+ final Map<String, String> map = new HashMap<>( treeMap );
+ MapInterpolator.interpolate( map );
+
+ getResolvedMap().clear();
+ getResolvedMap().putAll( map );
+ }
+
+ /**
+ * Called when a definition source is opened.
+ *
+ * @param path Path to the definition source that was opened.
+ */
+ private void openDefinitions( final Path path ) {
+ try {
+ final DefinitionSource ds = createDefinitionSource( path );
+ setDefinitionSource( ds );
+ storeDefinitionSourceFilename( path );
+
+ final Tooltip tooltipPath = new Tooltip( path.toString() );
+ tooltipPath.setShowDelay( Duration.millis( 200 ) );
+
+ final DefinitionPane pane = getDefinitionPane();
+ pane.update( ds );
+ pane.addTreeChangeHandler( mTreeHandler );
+ pane.addKeyEventHandler( mDefinitionKeyHandler );
+ pane.filenameProperty().setValue( path.getFileName().toString() );
+ pane.setTooltip( tooltipPath );
+
+ interpolateResolvedMap();
+ } catch( final Exception e ) {
+ error( e );
+ }
+ }
+
+ private void exportDefinitions( final Path path ) {
+ try {
+ final DefinitionPane pane = getDefinitionPane();
+ final TreeItem<String> root = pane.getTreeView().getRoot();
+ final TreeItem<String> problemChild = pane.isTreeWellFormed();
+
+ if( problemChild == null ) {
+ getDefinitionSource().getTreeAdapter().export( root, path );
+ getNotifier().clear();
+ }
+ else {
+ final String msg = get( "yaml.error.tree.form",
+ problemChild.getValue() );
+ getNotifier().notify( msg );
+ }
+ } catch( final Exception e ) {
+ error( e );
+ }
+ }
+
+ private Path getDefinitionPath() {
+ final String source = getPreferences().get(
+ PERSIST_DEFINITION_SOURCE, "" );
+
+ return new File(
+ source.isBlank()
+ ? getSetting( "file.definition.default", "variables.yaml" )
+ : source
+ ).toPath();
+ }
+
+ private void restoreDefinitionPane() {
+ openDefinitions( getDefinitionPath() );
+ }
+
+ private void storeDefinitionSourceFilename( final Path path ) {
+ getPreferences().put( PERSIST_DEFINITION_SOURCE, path.toString() );
+ }
+
+ /**
+ * Called when the last open tab is closed to clear the preview pane.
+ */
+ private void closeRemainingTab() {
+ getPreviewPane().clear();
+ }
+
+ /**
+ * Called when an exception occurs that warrants the user's attention.
+ *
+ * @param e The exception with a message that the user should know about.
+ */
+ private void error( final Exception e ) {
+ getNotifier().notify( e );
+ }
+
+ //---- File actions -------------------------------------------------------
+
+ /**
+ * Called when an observable instance has changed. This is called by both the
+ * snitch service and the notify service. The snitch service can be called for
+ * different file types, including definition sources.
+ *
+ * @param observable The observed instance.
+ * @param value The noteworthy item.
+ */
+ @Override
+ public void update( final Observable observable, final Object value ) {
+ if( value != null ) {
+ if( observable instanceof Snitch && value instanceof Path ) {
+ updateSelectedTab();
+ }
+ else if( observable instanceof Notifier && value instanceof String ) {
+ updateStatusBar( (String) value );
+ }
+ }
+ }
+
+ /**
+ * Updates the status bar to show the given message.
+ *
+ * @param s The message to show in the status bar.
+ */
+ private void updateStatusBar( final String s ) {
+ Platform.runLater(
+ () -> {
+ final int index = s.indexOf( '\n' );
+ final String message = s.substring(
+ 0, index > 0 ? index : s.length() );
+
+ getStatusBar().setText( message );
+ }
+ );
+ }
+
+ /**
+ * Called when a file has been modified.
+ */
+ private void updateSelectedTab() {
+ Platform.runLater(
+ () -> {
+ // Brute-force XSLT file reload by re-instantiating all processors.
+ resetProcessors();
+ refreshActiveTab();
+ }
+ );
+ }
+
+ /**
+ * After resetting the processors, they will refresh anew to be up-to-date
+ * with the files (text and definition) currently loaded into the editor.
+ */
+ private void resetProcessors() {
+ getProcessors().clear();
+ }
+
+ //---- 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 fileSaveAs() {
+ final FileEditorTab editor = getActiveFileEditor();
+ getFileEditorPane().saveEditorAs( editor );
+ getProcessors().remove( editor );
+
+ try {
+ refreshSelectedTab( editor );
+ } catch( final Exception ex ) {
+ getNotifier().notify( ex );
+ }
+ }
+
+ private void fileSaveAll() {
+ getFileEditorPane().saveAllEditors();
+ }
+
+ private void fileExit() {
+ final Window window = getWindow();
+ fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
+ }
+
+ //---- R menu actions
+ private void rScript() {
+ final String script = getPreferences().get( PERSIST_R_STARTUP, "" );
+ final RScriptDialog dialog = new RScriptDialog(
+ getWindow(), "Dialog.r.script.title", script );
+ final Optional<String> result = dialog.showAndWait();
+
+ result.ifPresent( this::putStartupScript );
+ }
+
+ private void rDirectory() {
+ final TextInputDialog dialog = new TextInputDialog(
+ getPreferences().get( PERSIST_R_DIRECTORY, USER_DIRECTORY )
+ );
+
+ dialog.setTitle( get( "Dialog.r.directory.title" ) );
+ dialog.setHeaderText( getLiteral( "Dialog.r.directory.header" ) );
+ dialog.setContentText( "Directory" );
+
+ final Optional<String> result = dialog.showAndWait();
+
+ result.ifPresent( this::putStartupDirectory );
+ }
+
+ /**
+ * Stores the R startup script into the user preferences.
+ */
+ private void putStartupScript( final String script ) {
+ putPreference( PERSIST_R_STARTUP, script );
+ }
+
+ /**
+ * Stores the R bootstrap script directory into the user preferences.
+ */
+ private void putStartupDirectory( final String directory ) {
+ putPreference( PERSIST_R_DIRECTORY, directory );
+ }
+
+ //---- Help actions -------------------------------------------------------
+ private void helpAbout() {
+ final Alert alert = new Alert( AlertType.INFORMATION );
+ alert.setTitle( get( "Dialog.about.title" ) );
+ alert.setHeaderText( get( "Dialog.about.header" ) );
+ alert.setContentText( get( "Dialog.about.content" ) );
+ alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
+ alert.initOwner( getWindow() );
+
+ alert.showAndWait();
+ }
+
+ //---- Convenience accessors ----------------------------------------------
+ private float getFloat( final String key, final float defaultValue ) {
+ return getPreferences().getFloat( key, defaultValue );
+ }
+
+ private Preferences getPreferences() {
+ return getOptions().getState();
+ }
+
+ protected Scene getScene() {
+ return mScene;
+ }
+
+ public Window getWindow() {
+ return getScene().getWindow();
+ }
+
+ private MarkdownEditorPane getActiveEditor() {
+ final EditorPane pane = getActiveFileEditor().getEditorPane();
+
+ return pane instanceof MarkdownEditorPane
+ ? (MarkdownEditorPane) pane
+ : null;
+ }
+
+ private FileEditorTab getActiveFileEditor() {
+ return getFileEditorPane().getActiveFileEditor();
+ }
+
+ //---- Member accessors ---------------------------------------------------
+
+ private Map<FileEditorTab, Processor<String>> getProcessors() {
+ return mProcessors;
+ }
+
+ private FileEditorTabPane getFileEditorPane() {
+ if( this.fileEditorPane == null ) {
+ this.fileEditorPane = createFileEditorPane();
+ }
+
+ return this.fileEditorPane;
+ }
+
+ private HTMLPreviewPane getPreviewPane() {
+ return mPreviewPane;
+ }
+
+ private void setDefinitionSource( final DefinitionSource definitionSource ) {
+ assert definitionSource != null;
+ mDefinitionSource = definitionSource;
+ }
+
+ private DefinitionSource getDefinitionSource() {
+ return mDefinitionSource;
+ }
+
+ private DefinitionPane getDefinitionPane() {
+ return mDefinitionPane;
+ }
+
+ private Options getOptions() {
+ return mOptions;
+ }
+
+ private Snitch getSnitch() {
+ return mSnitch;
+ }
+
+ private Notifier getNotifier() {
+ return mNotifier;
+ }
+
+ private Text getLineNumberText() {
+ return mLineNumberText;
+ }
+
+ private StatusBar getStatusBar() {
+ return mStatusBar;
+ }
+
+ private TextField getFindTextField() {
+ return mFindTextField;
+ }
+
+ //---- Member creators ----------------------------------------------------
+
+ /**
+ * Factory to create processors that are suited to different file types.
+ *
+ * @param tab The tab that is subjected to processing.
+ * @return A processor suited to the file type specified by the tab's path.
+ */
+ private Processor<String> createProcessor( final FileEditorTab tab ) {
+ return createProcessorFactory().createProcessor( tab );
+ }
+
+ private ProcessorFactory createProcessorFactory() {
+ return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
+ }
+
+ private DefinitionSource createDefaultDefinitionSource() {
+ return new YamlDefinitionSource( getDefinitionPath() );
+ }
+
+ private DefinitionSource createDefinitionSource( final Path path ) {
+ try {
+ return createDefinitionFactory().createDefinitionSource( path );
+ } catch( final Exception ex ) {
+ error( ex );
+ return createDefaultDefinitionSource();
+ }
+ }
+
+ private TextField createFindTextField() {
+ return new TextField();
+ }
+
+ /**
+ * Create an editor pane to hold file editor tabs.
+ *
+ * @return A new instance, never null.
+ */
+ private FileEditorTabPane createFileEditorPane() {
+ return new FileEditorTabPane();
+ }
+
+ private DefinitionFactory createDefinitionFactory() {
+ return new DefinitionFactory();
+ }
+
+ private StatusBar createStatusBar() {
+ return new StatusBar();
+ }
+
+ private Scene createScene() {
+ 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 ) );
+
+ getDefinitionPane().prefHeightProperty().bind( splitPane.heightProperty() );
+
+ final BorderPane borderPane = new BorderPane();
+ borderPane.setPrefSize( 1024, 800 );
+ borderPane.setTop( createMenuBar() );
+ borderPane.setBottom( getStatusBar() );
+ borderPane.setCenter( splitPane );
+
+ final VBox statusBar = new VBox();
+ statusBar.setAlignment( Pos.BASELINE_CENTER );
+ statusBar.getChildren().add( getLineNumberText() );
+ getStatusBar().getRightItems().add( statusBar );
return new Scene( borderPane );