Dave Jarvis' Repositories

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

Propagate changes from the definitions to an event handler

Author DaveJarvis <email>
Date 2020-06-03 21:00:08 GMT-0700
Commit 1205216b0cd1a283f010fa465b4f5dd28aafdeae
Parent 564bf6c
Delta 2397 lines added, 2320 lines removed, 77-line increase
.idea/workspace.xml
<component name="ChangeListManager">
<list default="true" id="3dcf7c8f-87b5-4d25-a804-39da40a621b8" name="Default Changelist" comment="">
- <change afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/DocumentParser.java" afterDir="false" />
- <change afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/EmptyTreeAdapter.java" afterDir="false" />
- <change afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/TreeAdapter.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/FileEditorTab.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/FileEditorTab.java" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/FileEditorTabPane.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/FileEditorTabPane.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/MainWindow.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/MainWindow.java" afterDir="false" />
- <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/AbstractDefinitionSource.java" beforeDir="false" />
+ <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/DefinitionFactory.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/DefinitionFactory.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/DefinitionPane.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/DefinitionPane.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/DefinitionSource.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/DefinitionSource.java" afterDir="false" />
- <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/EmptyDefinitionSource.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/EmptyDefinitionSource.java" afterDir="false" />
- <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/FileDefinitionSource.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/FileDefinitionSource.java" afterDir="false" />
- <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/yaml/YamlFileDefinitionSource.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/yaml/YamlFileDefinitionSource.java" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/EmptyDefinitionSource.java" beforeDir="false" />
+ <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/EmptyTreeAdapter.java" beforeDir="false" />
+ <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/VariableTreeItem.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/VariableTreeItem.java" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/yaml/YamlFileDefinitionSource.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/yaml/YamlDefinitionSource.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/yaml/YamlParser.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/yaml/YamlParser.java" afterDir="false" />
- <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/yaml/YamlTreeAdapter.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/yaml/YamlTreeAdapter.java" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/yaml/resolvers/ResolverYAMLFactory.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/yaml/resolvers/ResolverYamlFactory.java" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/yaml/resolvers/ResolverYAMLGenerator.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/definition/yaml/resolvers/ResolverYamlGenerator.java" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/editors/VariableNameInjector.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/editors/VariableNameInjector.java" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/util/StageState.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/util/StageState.java" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/src/main/resources/com/scrivenvar/messages.properties" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/resources/com/scrivenvar/messages.properties" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/src/main/resources/com/scrivenvar/settings.properties" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/resources/com/scrivenvar/settings.properties" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
</state>
<state x="285" y="311" key="#com.intellij.execution.impl.EditConfigurationsDialog/0.28.2560.1529@0.28.2560.1529" timestamp="1590982613111" />
- <state x="635" y="363" width="376" height="578" key="#com.intellij.ide.util.MemberChooser" timestamp="1591152784217">
+ <state x="635" y="363" width="376" height="578" key="#com.intellij.ide.util.MemberChooser" timestamp="1591238705460">
<screen x="0" y="28" width="2560" height="1529" />
</state>
- <state x="635" y="363" width="376" height="578" key="#com.intellij.ide.util.MemberChooser/0.28.2560.1529@0.28.2560.1529" timestamp="1591152784217" />
- <state x="468" y="28" width="711" height="1526" key="#com.intellij.refactoring.rename.AutomaticRenamingDialog" timestamp="1591145866446">
+ <state x="635" y="363" width="376" height="578" key="#com.intellij.ide.util.MemberChooser/0.28.2560.1529@0.28.2560.1529" timestamp="1591238705460" />
+ <state x="468" y="28" width="711" height="1526" key="#com.intellij.refactoring.rename.AutomaticRenamingDialog" timestamp="1591156855857">
<screen x="0" y="28" width="2560" height="1529" />
</state>
- <state x="468" y="28" width="711" height="1526" key="#com.intellij.refactoring.rename.AutomaticRenamingDialog/0.28.2560.1529@0.28.2560.1529" timestamp="1591145866446" />
- <state x="610" y="411" width="426" height="481" key="FileChooserDialogImpl" timestamp="1589659107517">
+ <state x="468" y="28" width="711" height="1526" key="#com.intellij.refactoring.rename.AutomaticRenamingDialog/0.28.2560.1529@0.28.2560.1529" timestamp="1591156855857" />
+ <state x="599" y="448" key="#xdebugger.evaluate" timestamp="1591240535031">
<screen x="0" y="28" width="2560" height="1529" />
</state>
- <state x="610" y="411" width="426" height="481" key="FileChooserDialogImpl/0.28.2560.1529@0.28.2560.1529" timestamp="1589659107517" />
- <state width="1573" height="321" key="GridCell.Tab.0.bottom" timestamp="1591153745029">
+ <state x="599" y="448" key="#xdebugger.evaluate/0.28.2560.1529@0.28.2560.1529" timestamp="1591240535031" />
+ <state x="610" y="411" width="426" height="481" key="FileChooserDialogImpl" timestamp="1591159062898">
<screen x="0" y="28" width="2560" height="1529" />
</state>
- <state width="1573" height="321" key="GridCell.Tab.0.bottom/0.28.2560.1529@0.28.2560.1529" timestamp="1591153745029" />
- <state width="1573" height="321" key="GridCell.Tab.0.center" timestamp="1591153745028">
+ <state x="610" y="411" width="426" height="481" key="FileChooserDialogImpl/0.28.2560.1529@0.28.2560.1529" timestamp="1591159062898" />
+ <state width="1573" height="396" key="GridCell.Tab.0.bottom" timestamp="1591242203228">
<screen x="0" y="28" width="2560" height="1529" />
</state>
- <state width="1573" height="321" key="GridCell.Tab.0.center/0.28.2560.1529@0.28.2560.1529" timestamp="1591153745028" />
- <state width="1573" height="321" key="GridCell.Tab.0.left" timestamp="1591153745028">
+ <state width="1573" height="396" key="GridCell.Tab.0.bottom/0.28.2560.1529@0.28.2560.1529" timestamp="1591242203228" />
+ <state width="1573" height="396" key="GridCell.Tab.0.center" timestamp="1591242203228">
<screen x="0" y="28" width="2560" height="1529" />
</state>
- <state width="1573" height="321" key="GridCell.Tab.0.left/0.28.2560.1529@0.28.2560.1529" timestamp="1591153745028" />
- <state width="1573" height="321" key="GridCell.Tab.0.right" timestamp="1591153745029">
+ <state width="1573" height="396" key="GridCell.Tab.0.center/0.28.2560.1529@0.28.2560.1529" timestamp="1591242203228" />
+ <state width="1573" height="396" key="GridCell.Tab.0.left" timestamp="1591242203227">
<screen x="0" y="28" width="2560" height="1529" />
</state>
- <state width="1573" height="321" key="GridCell.Tab.0.right/0.28.2560.1529@0.28.2560.1529" timestamp="1591153745029" />
- <state width="1573" height="396" key="GridCell.Tab.1.bottom" timestamp="1591138237719">
+ <state width="1573" height="396" key="GridCell.Tab.0.left/0.28.2560.1529@0.28.2560.1529" timestamp="1591242203227" />
+ <state width="1573" height="396" key="GridCell.Tab.0.right" timestamp="1591242203314">
<screen x="0" y="28" width="2560" height="1529" />
</state>
- <state width="1573" height="396" key="GridCell.Tab.1.bottom/0.28.2560.1529@0.28.2560.1529" timestamp="1591138237719" />
- <state width="1573" height="396" key="GridCell.Tab.1.center" timestamp="1591138237718">
+ <state width="1573" height="396" key="GridCell.Tab.0.right/0.28.2560.1529@0.28.2560.1529" timestamp="1591242203314" />
+ <state width="1573" height="396" key="GridCell.Tab.1.bottom" timestamp="1591242203229">
<screen x="0" y="28" width="2560" height="1529" />
</state>
- <state width="1573" height="396" key="GridCell.Tab.1.center/0.28.2560.1529@0.28.2560.1529" timestamp="1591138237718" />
- <state width="1573" height="396" key="GridCell.Tab.1.left" timestamp="1591138237718">
+ <state width="1573" height="396" key="GridCell.Tab.1.bottom/0.28.2560.1529@0.28.2560.1529" timestamp="1591242203229" />
+ <state width="1573" height="396" key="GridCell.Tab.1.center" timestamp="1591242203229">
<screen x="0" y="28" width="2560" height="1529" />
</state>
- <state width="1573" height="396" key="GridCell.Tab.1.left/0.28.2560.1529@0.28.2560.1529" timestamp="1591138237718" />
- <state width="1573" height="396" key="GridCell.Tab.1.right" timestamp="1591138237719">
+ <state width="1573" height="396" key="GridCell.Tab.1.center/0.28.2560.1529@0.28.2560.1529" timestamp="1591242203229" />
+ <state width="1573" height="396" key="GridCell.Tab.1.left" timestamp="1591242203229">
<screen x="0" y="28" width="2560" height="1529" />
</state>
- <state width="1573" height="396" key="GridCell.Tab.1.right/0.28.2560.1529@0.28.2560.1529" timestamp="1591138237719" />
+ <state width="1573" height="396" key="GridCell.Tab.1.left/0.28.2560.1529@0.28.2560.1529" timestamp="1591242203229" />
+ <state width="1573" height="396" key="GridCell.Tab.1.right" timestamp="1591242203229">
+ <screen x="0" y="28" width="2560" height="1529" />
+ </state>
+ <state width="1573" height="396" key="GridCell.Tab.1.right/0.28.2560.1529@0.28.2560.1529" timestamp="1591242203229" />
<state x="324" y="288" key="SettingsEditor" timestamp="1589576619807">
<screen x="0" y="28" width="2560" height="1529" />
</state>
<state x="714" y="633" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2/0.28.2560.1529@0.28.2560.1529" timestamp="1590804130551" />
- <state x="531" y="261" width="586" height="753" key="find.popup" timestamp="1591153463483">
+ <state x="1776" y="397" width="586" height="753" key="find.popup" timestamp="1591240407704">
<screen x="0" y="28" width="2560" height="1529" />
</state>
- <state x="531" y="261" width="586" height="753" key="find.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1591153463483" />
+ <state x="1776" y="397" width="586" height="753" key="find.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1591240407704" />
<state x="533" y="414" width="581" height="476" key="refactoring.ChangeSignatureDialog" timestamp="1589663937037">
<screen x="0" y="28" width="2560" height="1529" />
</state>
<state x="533" y="414" width="581" height="476" key="refactoring.ChangeSignatureDialog/0.28.2560.1529@0.28.2560.1529" timestamp="1589663937037" />
- <state x="490" y="304" key="run.anything.popup" timestamp="1591139048305">
+ <state x="490" y="304" key="run.anything.popup" timestamp="1591240533110">
<screen x="0" y="28" width="2560" height="1529" />
</state>
- <state x="490" y="304" key="run.anything.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1591139048305" />
- <state x="490" y="327" width="672" height="678" key="search.everywhere.popup" timestamp="1591146276788">
+ <state x="490" y="304" key="run.anything.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1591240533110" />
+ <state x="490" y="327" width="672" height="678" key="search.everywhere.popup" timestamp="1591240272617">
<screen x="0" y="28" width="2560" height="1529" />
</state>
- <state x="490" y="327" width="672" height="678" key="search.everywhere.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1591146276788" />
+ <state x="490" y="327" width="672" height="678" key="search.everywhere.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1591240272617" />
+ </component>
+ <component name="XDebuggerManager">
+ <breakpoint-manager>
+ <breakpoints>
+ <line-breakpoint enabled="true" type="java-line">
+ <url>file://$PROJECT_DIR$/src/main/java/com/scrivenvar/MainWindow.java</url>
+ <line>732</line>
+ <option name="timeStamp" value="49" />
+ </line-breakpoint>
+ </breakpoints>
+ </breakpoint-manager>
</component>
<component name="masterDetails">
src/main/java/com/scrivenvar/FileEditorTab.java
*/
private String getTabTitle() {
- final Path filePath = getPath();
-
- return (filePath == null)
- ? Messages.get( "FileEditor.untitled" )
- : filePath.getFileName().toString();
+ return getPath().getFileName().toString();
}
src/main/java/com/scrivenvar/FileEditorTabPane.java
".filter";
- private final Options options = Services.load( Options.class );
- private final Settings settings = Services.load( Settings.class );
- private final Notifier notifyService = Services.load( Notifier.class );
-
- private final ReadOnlyObjectWrapper<Path> openDefinition =
- new ReadOnlyObjectWrapper<>();
- private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor =
- new ReadOnlyObjectWrapper<>();
- private final ReadOnlyBooleanWrapper anyFileEditorModified =
- new ReadOnlyBooleanWrapper();
-
- /**
- * Constructs a new file editor tab pane.
- */
- public FileEditorTabPane() {
- final ObservableList<Tab> tabs = getTabs();
-
- setFocusTraversable( false );
- setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
-
- addTabSelectionListener(
- ( ObservableValue<? extends Tab> tabPane,
- final Tab oldTab, final Tab newTab ) -> {
-
- if( newTab != null ) {
- mActiveFileEditor.set( (FileEditorTab) newTab );
- }
- }
- );
-
- final ChangeListener<Boolean> modifiedListener = ( observable, oldValue,
- newValue ) -> {
- for( final Tab tab : tabs ) {
- if( ((FileEditorTab) tab).isModified() ) {
- this.anyFileEditorModified.set( true );
- break;
- }
- }
- };
-
- tabs.addListener(
- (ListChangeListener<Tab>) change -> {
- while( change.next() ) {
- if( change.wasAdded() ) {
- change.getAddedSubList().forEach(
- ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
- .addListener( modifiedListener ) );
- }
- else if( change.wasRemoved() ) {
- change.getRemoved().forEach(
- ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
- .removeListener(
- modifiedListener ) );
- }
- }
-
- // Changes in the tabs may also change anyFileEditorModified property
- // (e.g. closed modified file)
- modifiedListener.changed( null, null, null );
- }
- );
- }
-
- /**
- * Allows observers to be notified when the current file editor tab changes.
- *
- * @param listener The listener to notify of tab change events.
- */
- public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
- // Observe the tab so that when a new tab is opened or selected,
- // a notification is kicked off.
- getSelectionModel().selectedItemProperty().addListener( listener );
- }
-
- /**
- * Allows clients to manipulate the editor content directly.
- *
- * @return The text area for the active file editor.
- */
- public StyledTextArea getEditor() {
- return getActiveFileEditor().getEditorPane().getEditor();
- }
-
- /**
- * Returns the tab that has keyboard focus.
- *
- * @return A non-null instance.
- */
- public FileEditorTab getActiveFileEditor() {
- return mActiveFileEditor.get();
- }
-
- /**
- * Returns the property corresponding to the tab that has focus.
- *
- * @return A non-null instance.
- */
- public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
- return mActiveFileEditor.getReadOnlyProperty();
- }
-
- /**
- * Property that can answer whether the text has been modified.
- *
- * @return A non-null instance, true meaning the content has not been saved.
- */
- ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
- return this.anyFileEditorModified.getReadOnlyProperty();
- }
-
- /**
- * Creates a new editor instance from the given path.
- *
- * @param path The file to open.
- * @return A non-null instance.
- */
- private FileEditorTab createFileEditor( final Path path ) {
- assert path != null;
-
- final FileEditorTab tab = new FileEditorTab( path );
-
- tab.setOnCloseRequest( e -> {
- if( !canCloseEditor( tab ) ) {
- e.consume();
- }
- else if( isActiveFileEditor( tab ) ) {
- // Prevent prompting the user to save when there are no file editor
- // tabs open.
- mActiveFileEditor.set( null );
- }
- } );
-
- return tab;
- }
-
- private boolean isActiveFileEditor( final FileEditorTab tab ) {
- return getActiveFileEditor() == tab;
- }
-
- private Path getDefaultPath() {
- final String filename = getDefaultFilename();
- return (new File( filename )).toPath();
- }
-
- private String getDefaultFilename() {
- return getSettings().getSetting( "file.default", "untitled.md" );
- }
-
- /**
- * Called when the user selects New from the File menu.
- */
- void newEditor() {
- final Path defaultPath = getDefaultPath();
- final FileEditorTab tab = createFileEditor( defaultPath );
-
- getTabs().add( tab );
- getSelectionModel().select( tab );
- }
-
- void openFileDialog() {
- final String title = get( "Dialog.file.choose.open.title" );
- final FileChooser dialog = createFileChooser( title );
- final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
-
- if( files != null ) {
- openFiles( files );
- }
- }
-
- /**
- * Opens the files into new editors, unless one of those files was a
- * definition file. The definition file is loaded into the definition pane,
- * but only the first one selected (multiple definition files will result in a
- * warning).
- *
- * @param files The list of non-definition files that the were requested to
- * open.
- */
- private void openFiles( final List<File> files ) {
- final List<String> extensions =
- createExtensionFilter( DEFINITION ).getExtensions();
- final FileTypePredicate predicate =
- new FileTypePredicate( extensions );
-
- // The user might have opened multiple definitions files. These will
- // be discarded from the text editable files.
- final List<File> definitions
- = files.stream().filter( predicate ).collect( Collectors.toList() );
-
- // Create a modifiable list to remove any definition files that were
- // opened.
- final List<File> editors = new ArrayList<>( files );
-
- if( !editors.isEmpty() ) {
- saveLastDirectory( editors.get( 0 ) );
- }
-
- editors.removeAll( definitions );
-
- // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
- if( !editors.isEmpty() ) {
- openEditors( editors, 0 );
- }
-
- if( !definitions.isEmpty() ) {
- openDefinition( definitions.get( 0 ) );
- }
- }
-
- private void openEditors( final List<File> files, final int activeIndex ) {
- final int fileTally = files.size();
- final List<Tab> tabs = getTabs();
-
- // Close single unmodified "Untitled" tab.
- if( tabs.size() == 1 ) {
- final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 ));
-
- if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
- closeEditor( fileEditor, false );
- }
- }
-
- for( int i = 0; i < fileTally; i++ ) {
- final Path path = files.get( i ).toPath();
-
- FileEditorTab fileEditorTab = findEditor( path );
-
- // Only open new files.
- if( fileEditorTab == null ) {
- fileEditorTab = createFileEditor( path );
- getTabs().add( fileEditorTab );
- }
-
- // Select the first file in the list.
- if( i == activeIndex ) {
- getSelectionModel().select( fileEditorTab );
- }
- }
- }
-
- /**
- * Returns a property that changes when a new definition file is opened.
- *
- * @return The path to a definition file that was opened.
- */
- public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
- return getOnOpenDefinitionFile().getReadOnlyProperty();
- }
-
- private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
- return this.openDefinition;
- }
-
- /**
- * Called when the user has opened a definition file (using the file open
- * dialog box). This will replace the current set of definitions for the
- * active tab.
- *
- * @param definition The file to open.
- */
- private void openDefinition( final File definition ) {
- // TODO: Prevent reading this file twice when a new text document is opened.
- // (might be a matter of checking the value first).
- getOnOpenDefinitionFile().set( definition.toPath() );
- }
-
- /**
- * Called when the contents of the editor are to be saved.
- *
- * @param tab The tab containing content to save.
- * @return true The contents were saved (or needn't be saved).
- */
- public boolean saveEditor( final FileEditorTab tab ) {
- if( tab == null || !tab.isModified() ) {
- return true;
- }
-
- return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
- }
-
- /**
- * Opens the Save As dialog for the user to save the content under a new
- * path.
- *
- * @param tab The tab with contents to save.
- * @return true The contents were saved, or the tab was null.
- */
- public boolean saveEditorAs( final FileEditorTab tab ) {
- if( tab == null ) {
- return true;
- }
-
- getSelectionModel().select( tab );
-
- final FileChooser fileChooser = createFileChooser( get(
- "Dialog.file.choose.save.title" ) );
- final File file = fileChooser.showSaveDialog( getWindow() );
- if( file == null ) {
- return false;
- }
-
- saveLastDirectory( file );
- tab.setPath( file.toPath() );
-
- return tab.save();
- }
-
- void saveAllEditors() {
- for( final FileEditorTab fileEditor : getAllEditors() ) {
- saveEditor( fileEditor );
- }
- }
-
- /**
- * Answers whether the file has had modifications. '
- *
- * @param tab THe tab to check for modifications.
- * @return false The file is unmodified.
- */
- boolean canCloseEditor( final FileEditorTab tab ) {
- final AtomicReference<Boolean> canClose = new AtomicReference<>();
- canClose.set( true );
-
- if( tab.isModified() ) {
- final Notification message = getNotifyService().createNotification(
- Messages.get( "Alert.file.close.title" ),
- Messages.get( "Alert.file.close.text" ),
- tab.getText()
- );
-
- final Alert confirmSave = getNotifyService().createConfirmation(
- getWindow(), message );
-
- final Optional<ButtonType> buttonType = confirmSave.showAndWait();
-
- buttonType.ifPresent(
- save -> canClose.set(
- save == YES ? saveEditor( tab ) : save == ButtonType.NO
- )
- );
- }
-
- return canClose.get();
- }
-
- private Notifier getNotifyService() {
- return this.notifyService;
- }
-
- boolean closeEditor( final FileEditorTab tab, final boolean save ) {
- if( tab == null ) {
- return true;
- }
-
- if( save ) {
- Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
- Event.fireEvent( tab, event );
-
- if( event.isConsumed() ) {
- return false;
- }
- }
-
- getTabs().remove( tab );
-
- if( tab.getOnClosed() != null ) {
- Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
- }
-
- return true;
- }
-
- boolean closeAllEditors() {
- final FileEditorTab[] allEditors = getAllEditors();
- final FileEditorTab activeEditor = getActiveFileEditor();
-
- // try to save active tab first because in case the user decides to cancel,
- // then it stays active
- if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
- return false;
- }
-
- // This should be called any time a tab changes.
- persistPreferences();
-
- // save modified tabs
- for( int i = 0; i < allEditors.length; i++ ) {
- final FileEditorTab fileEditor = allEditors[ i ];
-
- if( fileEditor == activeEditor ) {
- continue;
- }
-
- if( fileEditor.isModified() ) {
- // activate the modified tab to make its modified content visible to
- // the user
- getSelectionModel().select( i );
-
- if( !canCloseEditor( fileEditor ) ) {
- return false;
- }
- }
- }
-
- // Close all tabs.
- for( final FileEditorTab fileEditor : allEditors ) {
- if( !closeEditor( fileEditor, false ) ) {
- return false;
- }
- }
-
- return getTabs().isEmpty();
- }
-
- private FileEditorTab[] getAllEditors() {
- final ObservableList<Tab> tabs = getTabs();
- final int length = tabs.size();
- final FileEditorTab[] allEditors = new FileEditorTab[ length ];
-
- for( int i = 0; i < length; i++ ) {
- allEditors[ i ] = (FileEditorTab) tabs.get( i );
- }
-
- return allEditors;
- }
-
- /**
- * Returns the file editor tab that has the given path.
- *
- * @return null No file editor tab for the given path was found.
- */
- private FileEditorTab findEditor( final Path path ) {
- for( final Tab tab : getTabs() ) {
- final FileEditorTab fileEditor = (FileEditorTab) tab;
-
- if( fileEditor.isPath( path ) ) {
- return fileEditor;
- }
- }
-
- return null;
- }
-
- private FileChooser createFileChooser( String title ) {
- final FileChooser fileChooser = new FileChooser();
-
- fileChooser.setTitle( title );
- fileChooser.getExtensionFilters().addAll(
- createExtensionFilters() );
-
- final String lastDirectory = getPreferences().get( "lastDirectory", null );
- File file = new File( (lastDirectory != null) ? lastDirectory : "." );
-
- if( !file.isDirectory() ) {
- file = new File( "." );
- }
-
- fileChooser.setInitialDirectory( file );
- return fileChooser;
- }
-
- private List<ExtensionFilter> createExtensionFilters() {
- final List<ExtensionFilter> list = new ArrayList<>();
-
- // TODO: Return a list of all properties that match the filter prefix.
- // This will allow dynamic filters to be added and removed just by
- // updating the properties file.
- list.add( createExtensionFilter( SOURCE ) );
- list.add( createExtensionFilter( DEFINITION ) );
- list.add( createExtensionFilter( XML ) );
- list.add( createExtensionFilter( ALL ) );
- return list;
- }
-
- /**
- * Returns a filter for file name extensions recognized by the application
- * that can be opened by the user.
- *
- * @param filetype Used to find the globbing pattern for extensions.
- * @return A filename filter suitable for use by a FileDialog instance.
- */
- private ExtensionFilter createExtensionFilter( final FileType filetype ) {
- final String tKey = String.format( "%s.title.%s",
- FILTER_EXTENSION_TITLES,
- filetype );
- final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype );
-
- return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
- }
-
- private List<String> getExtensions( final String key ) {
- return getSettings().getStringSettingList( key );
- }
-
- private void saveLastDirectory( final File file ) {
- getPreferences().put( "lastDirectory", file.getParent() );
- }
-
- public void restorePreferences() {
- int activeIndex = 0;
-
- final Preferences preferences = getPreferences();
- final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
- final String activeFileName = preferences.get( "activeFile", null );
-
- final ArrayList<File> files = new ArrayList<>( fileNames.length );
-
- for( final String fileName : fileNames ) {
- final File file = new File( fileName );
-
- if( file.exists() ) {
- files.add( file );
-
- if( fileName.equals( activeFileName ) ) {
- activeIndex = files.size() - 1;
- }
- }
- }
-
- if( files.isEmpty() ) {
- newEditor();
- }
- else {
- openEditors( files, activeIndex );
- }
- }
-
- public void persistPreferences() {
- final ObservableList<Tab> allEditors = getTabs();
- final List<String> fileNames = new ArrayList<>( allEditors.size() );
-
- for( final Tab tab : allEditors ) {
- final FileEditorTab fileEditor = (FileEditorTab) tab;
- final Path filePath = fileEditor.getPath();
-
- if( filePath != null ) {
- fileNames.add( filePath.toString() );
- }
- }
-
- final Preferences preferences = getPreferences();
- Utils.putPrefsStrings( preferences,
- "file",
- fileNames.toArray( new String[ 0 ] ) );
-
- final FileEditorTab activeEditor = getActiveFileEditor();
- final Path filePath = activeEditor == null ? null : activeEditor.getPath();
-
- if( filePath == null ) {
- preferences.remove( "activeFile" );
- }
- else {
- preferences.put( "activeFile", filePath.toString() );
- }
- }
-
- private Settings getSettings() {
- return this.settings;
- }
-
- protected Options getOptions() {
- return this.options;
+ private final Options mOptions = Services.load( Options.class );
+ private final Settings mSettings = Services.load( Settings.class );
+ private final Notifier mNotifyService = Services.load( Notifier.class );
+
+ private final ReadOnlyObjectWrapper<Path> openDefinition =
+ new ReadOnlyObjectWrapper<>();
+ private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor =
+ new ReadOnlyObjectWrapper<>();
+ private final ReadOnlyBooleanWrapper anyFileEditorModified =
+ new ReadOnlyBooleanWrapper();
+
+ /**
+ * Constructs a new file editor tab pane.
+ */
+ public FileEditorTabPane() {
+ final ObservableList<Tab> tabs = getTabs();
+
+ setFocusTraversable( false );
+ setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
+
+ addTabSelectionListener(
+ ( ObservableValue<? extends Tab> tabPane,
+ final Tab oldTab, final Tab newTab ) -> {
+
+ if( newTab != null ) {
+ mActiveFileEditor.set( (FileEditorTab) newTab );
+ }
+ }
+ );
+
+ final ChangeListener<Boolean> modifiedListener = ( observable, oldValue,
+ newValue ) -> {
+ for( final Tab tab : tabs ) {
+ if( ((FileEditorTab) tab).isModified() ) {
+ this.anyFileEditorModified.set( true );
+ break;
+ }
+ }
+ };
+
+ tabs.addListener(
+ (ListChangeListener<Tab>) change -> {
+ while( change.next() ) {
+ if( change.wasAdded() ) {
+ change.getAddedSubList().forEach(
+ ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
+ .addListener( modifiedListener ) );
+ }
+ else if( change.wasRemoved() ) {
+ change.getRemoved().forEach(
+ ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
+ .removeListener(
+ modifiedListener ) );
+ }
+ }
+
+ // Changes in the tabs may also change anyFileEditorModified property
+ // (e.g. closed modified file)
+ modifiedListener.changed( null, null, null );
+ }
+ );
+ }
+
+ /**
+ * Allows observers to be notified when the current file editor tab changes.
+ *
+ * @param listener The listener to notify of tab change events.
+ */
+ public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
+ // Observe the tab so that when a new tab is opened or selected,
+ // a notification is kicked off.
+ getSelectionModel().selectedItemProperty().addListener( listener );
+ }
+
+ /**
+ * Allows clients to manipulate the editor content directly.
+ *
+ * @return The text area for the active file editor.
+ */
+ public StyledTextArea getEditor() {
+ return getActiveFileEditor().getEditorPane().getEditor();
+ }
+
+ /**
+ * Returns the tab that has keyboard focus.
+ *
+ * @return A non-null instance.
+ */
+ public FileEditorTab getActiveFileEditor() {
+ return mActiveFileEditor.get();
+ }
+
+ /**
+ * Returns the property corresponding to the tab that has focus.
+ *
+ * @return A non-null instance.
+ */
+ public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
+ return mActiveFileEditor.getReadOnlyProperty();
+ }
+
+ /**
+ * Property that can answer whether the text has been modified.
+ *
+ * @return A non-null instance, true meaning the content has not been saved.
+ */
+ ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
+ return this.anyFileEditorModified.getReadOnlyProperty();
+ }
+
+ /**
+ * Creates a new editor instance from the given path.
+ *
+ * @param path The file to open.
+ * @return A non-null instance.
+ */
+ private FileEditorTab createFileEditor( final Path path ) {
+ assert path != null;
+
+ final FileEditorTab tab = new FileEditorTab( path );
+
+ tab.setOnCloseRequest( e -> {
+ if( !canCloseEditor( tab ) ) {
+ e.consume();
+ }
+ else if( isActiveFileEditor( tab ) ) {
+ // Prevent prompting the user to save when there are no file editor
+ // tabs open.
+ mActiveFileEditor.set( null );
+ }
+ } );
+
+ return tab;
+ }
+
+ private boolean isActiveFileEditor( final FileEditorTab tab ) {
+ return getActiveFileEditor() == tab;
+ }
+
+ private Path getDefaultPath() {
+ final String filename = getDefaultFilename();
+ return (new File( filename )).toPath();
+ }
+
+ private String getDefaultFilename() {
+ return getSettings().getSetting( "file.default", "untitled.md" );
+ }
+
+ /**
+ * Called when the user selects New from the File menu.
+ */
+ void newEditor() {
+ final Path defaultPath = getDefaultPath();
+ final FileEditorTab tab = createFileEditor( defaultPath );
+
+ getTabs().add( tab );
+ getSelectionModel().select( tab );
+ }
+
+ void openFileDialog() {
+ final String title = get( "Dialog.file.choose.open.title" );
+ final FileChooser dialog = createFileChooser( title );
+ final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
+
+ if( files != null ) {
+ openFiles( files );
+ }
+ }
+
+ /**
+ * Opens the files into new editors, unless one of those files was a
+ * definition file. The definition file is loaded into the definition pane,
+ * but only the first one selected (multiple definition files will result in a
+ * warning).
+ *
+ * @param files The list of non-definition files that the were requested to
+ * open.
+ */
+ private void openFiles( final List<File> files ) {
+ final List<String> extensions =
+ createExtensionFilter( DEFINITION ).getExtensions();
+ final FileTypePredicate predicate =
+ new FileTypePredicate( extensions );
+
+ // The user might have opened multiple definitions files. These will
+ // be discarded from the text editable files.
+ final List<File> definitions
+ = files.stream().filter( predicate ).collect( Collectors.toList() );
+
+ // Create a modifiable list to remove any definition files that were
+ // opened.
+ final List<File> editors = new ArrayList<>( files );
+
+ if( !editors.isEmpty() ) {
+ saveLastDirectory( editors.get( 0 ) );
+ }
+
+ editors.removeAll( definitions );
+
+ // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
+ if( !editors.isEmpty() ) {
+ openEditors( editors, 0 );
+ }
+
+ if( !definitions.isEmpty() ) {
+ openDefinition( definitions.get( 0 ) );
+ }
+ }
+
+ private void openEditors( final List<File> files, final int activeIndex ) {
+ final int fileTally = files.size();
+ final List<Tab> tabs = getTabs();
+
+ // Close single unmodified "Untitled" tab.
+ if( tabs.size() == 1 ) {
+ final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 ));
+
+ if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
+ closeEditor( fileEditor, false );
+ }
+ }
+
+ for( int i = 0; i < fileTally; i++ ) {
+ final Path path = files.get( i ).toPath();
+
+ FileEditorTab fileEditorTab = findEditor( path );
+
+ // Only open new files.
+ if( fileEditorTab == null ) {
+ fileEditorTab = createFileEditor( path );
+ getTabs().add( fileEditorTab );
+ }
+
+ // Select the first file in the list.
+ if( i == activeIndex ) {
+ getSelectionModel().select( fileEditorTab );
+ }
+ }
+ }
+
+ /**
+ * Returns a property that changes when a new definition file is opened.
+ *
+ * @return The path to a definition file that was opened.
+ */
+ public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
+ return getOnOpenDefinitionFile().getReadOnlyProperty();
+ }
+
+ private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
+ return this.openDefinition;
+ }
+
+ /**
+ * Called when the user has opened a definition file (using the file open
+ * dialog box). This will replace the current set of definitions for the
+ * active tab.
+ *
+ * @param definition The file to open.
+ */
+ private void openDefinition( final File definition ) {
+ // TODO: Prevent reading this file twice when a new text document is opened.
+ // (might be a matter of checking the value first).
+ getOnOpenDefinitionFile().set( definition.toPath() );
+ }
+
+ /**
+ * Called when the contents of the editor are to be saved.
+ *
+ * @param tab The tab containing content to save.
+ * @return true The contents were saved (or needn't be saved).
+ */
+ public boolean saveEditor( final FileEditorTab tab ) {
+ if( tab == null || !tab.isModified() ) {
+ return true;
+ }
+
+ return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
+ }
+
+ /**
+ * Opens the Save As dialog for the user to save the content under a new
+ * path.
+ *
+ * @param tab The tab with contents to save.
+ * @return true The contents were saved, or the tab was null.
+ */
+ public boolean saveEditorAs( final FileEditorTab tab ) {
+ if( tab == null ) {
+ return true;
+ }
+
+ getSelectionModel().select( tab );
+
+ final FileChooser fileChooser = createFileChooser( get(
+ "Dialog.file.choose.save.title" ) );
+ final File file = fileChooser.showSaveDialog( getWindow() );
+ if( file == null ) {
+ return false;
+ }
+
+ saveLastDirectory( file );
+ tab.setPath( file.toPath() );
+
+ return tab.save();
+ }
+
+ void saveAllEditors() {
+ for( final FileEditorTab fileEditor : getAllEditors() ) {
+ saveEditor( fileEditor );
+ }
+ }
+
+ /**
+ * Answers whether the file has had modifications. '
+ *
+ * @param tab THe tab to check for modifications.
+ * @return false The file is unmodified.
+ */
+ boolean canCloseEditor( final FileEditorTab tab ) {
+ final AtomicReference<Boolean> canClose = new AtomicReference<>();
+ canClose.set( true );
+
+ if( tab.isModified() ) {
+ final Notification message = getNotifyService().createNotification(
+ Messages.get( "Alert.file.close.title" ),
+ Messages.get( "Alert.file.close.text" ),
+ tab.getText()
+ );
+
+ final Alert confirmSave = getNotifyService().createConfirmation(
+ getWindow(), message );
+
+ final Optional<ButtonType> buttonType = confirmSave.showAndWait();
+
+ buttonType.ifPresent(
+ save -> canClose.set(
+ save == YES ? saveEditor( tab ) : save == ButtonType.NO
+ )
+ );
+ }
+
+ return canClose.get();
+ }
+
+ private Notifier getNotifyService() {
+ return this.mNotifyService;
+ }
+
+ boolean closeEditor( final FileEditorTab tab, final boolean save ) {
+ if( tab == null ) {
+ return true;
+ }
+
+ if( save ) {
+ Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
+ Event.fireEvent( tab, event );
+
+ if( event.isConsumed() ) {
+ return false;
+ }
+ }
+
+ getTabs().remove( tab );
+
+ if( tab.getOnClosed() != null ) {
+ Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
+ }
+
+ return true;
+ }
+
+ boolean closeAllEditors() {
+ final FileEditorTab[] allEditors = getAllEditors();
+ final FileEditorTab activeEditor = getActiveFileEditor();
+
+ // try to save active tab first because in case the user decides to cancel,
+ // then it stays active
+ if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
+ return false;
+ }
+
+ // This should be called any time a tab changes.
+ persistPreferences();
+
+ // save modified tabs
+ for( int i = 0; i < allEditors.length; i++ ) {
+ final FileEditorTab fileEditor = allEditors[ i ];
+
+ if( fileEditor == activeEditor ) {
+ continue;
+ }
+
+ if( fileEditor.isModified() ) {
+ // activate the modified tab to make its modified content visible to
+ // the user
+ getSelectionModel().select( i );
+
+ if( !canCloseEditor( fileEditor ) ) {
+ return false;
+ }
+ }
+ }
+
+ // Close all tabs.
+ for( final FileEditorTab fileEditor : allEditors ) {
+ if( !closeEditor( fileEditor, false ) ) {
+ return false;
+ }
+ }
+
+ return getTabs().isEmpty();
+ }
+
+ private FileEditorTab[] getAllEditors() {
+ final ObservableList<Tab> tabs = getTabs();
+ final int length = tabs.size();
+ final FileEditorTab[] allEditors = new FileEditorTab[ length ];
+
+ for( int i = 0; i < length; i++ ) {
+ allEditors[ i ] = (FileEditorTab) tabs.get( i );
+ }
+
+ return allEditors;
+ }
+
+ /**
+ * Returns the file editor tab that has the given path.
+ *
+ * @return null No file editor tab for the given path was found.
+ */
+ private FileEditorTab findEditor( final Path path ) {
+ for( final Tab tab : getTabs() ) {
+ final FileEditorTab fileEditor = (FileEditorTab) tab;
+
+ if( fileEditor.isPath( path ) ) {
+ return fileEditor;
+ }
+ }
+
+ return null;
+ }
+
+ private FileChooser createFileChooser( String title ) {
+ final FileChooser fileChooser = new FileChooser();
+
+ fileChooser.setTitle( title );
+ fileChooser.getExtensionFilters().addAll(
+ createExtensionFilters() );
+
+ final String lastDirectory = getPreferences().get( "lastDirectory", null );
+ File file = new File( (lastDirectory != null) ? lastDirectory : "." );
+
+ if( !file.isDirectory() ) {
+ file = new File( "." );
+ }
+
+ fileChooser.setInitialDirectory( file );
+ return fileChooser;
+ }
+
+ private List<ExtensionFilter> createExtensionFilters() {
+ final List<ExtensionFilter> list = new ArrayList<>();
+
+ // TODO: Return a list of all properties that match the filter prefix.
+ // This will allow dynamic filters to be added and removed just by
+ // updating the properties file.
+ list.add( createExtensionFilter( SOURCE ) );
+ list.add( createExtensionFilter( DEFINITION ) );
+ list.add( createExtensionFilter( XML ) );
+ list.add( createExtensionFilter( ALL ) );
+ return list;
+ }
+
+ /**
+ * Returns a filter for file name extensions recognized by the application
+ * that can be opened by the user.
+ *
+ * @param filetype Used to find the globbing pattern for extensions.
+ * @return A filename filter suitable for use by a FileDialog instance.
+ */
+ private ExtensionFilter createExtensionFilter( final FileType filetype ) {
+ final String tKey = String.format( "%s.title.%s",
+ FILTER_EXTENSION_TITLES,
+ filetype );
+ final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype );
+
+ return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
+ }
+
+ private List<String> getExtensions( final String key ) {
+ return getSettings().getStringSettingList( key );
+ }
+
+ private void saveLastDirectory( final File file ) {
+ getPreferences().put( "lastDirectory", file.getParent() );
+ }
+
+ public void restorePreferences() {
+ int activeIndex = 0;
+
+ final Preferences preferences = getPreferences();
+ final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
+ final String activeFileName = preferences.get( "activeFile", null );
+
+ final ArrayList<File> files = new ArrayList<>( fileNames.length );
+
+ for( final String fileName : fileNames ) {
+ final File file = new File( fileName );
+
+ if( file.exists() ) {
+ files.add( file );
+
+ if( fileName.equals( activeFileName ) ) {
+ activeIndex = files.size() - 1;
+ }
+ }
+ }
+
+ if( files.isEmpty() ) {
+ newEditor();
+ }
+ else {
+ openEditors( files, activeIndex );
+ }
+ }
+
+ public void persistPreferences() {
+ final ObservableList<Tab> allEditors = getTabs();
+ final List<String> fileNames = new ArrayList<>( allEditors.size() );
+
+ for( final Tab tab : allEditors ) {
+ final FileEditorTab fileEditor = (FileEditorTab) tab;
+ final Path filePath = fileEditor.getPath();
+
+ if( filePath != null ) {
+ fileNames.add( filePath.toString() );
+ }
+ }
+
+ final Preferences preferences = getPreferences();
+ Utils.putPrefsStrings( preferences,
+ "file",
+ fileNames.toArray( new String[ 0 ] ) );
+
+ final FileEditorTab activeEditor = getActiveFileEditor();
+ final Path filePath = activeEditor == null ? null : activeEditor.getPath();
+
+ if( filePath == null ) {
+ preferences.remove( "activeFile" );
+ }
+ else {
+ preferences.put( "activeFile", filePath.toString() );
+ }
+ }
+
+ private Settings getSettings() {
+ return mSettings;
+ }
+
+ protected Options getOptions() {
+ return mOptions;
}
src/main/java/com/scrivenvar/MainWindow.java
import com.scrivenvar.definition.*;
-import com.scrivenvar.dialogs.RScriptDialog;
-import com.scrivenvar.editors.EditorPane;
-import com.scrivenvar.editors.VariableNameInjector;
-import com.scrivenvar.editors.markdown.MarkdownEditorPane;
-import com.scrivenvar.predicates.files.FileTypePredicate;
-import com.scrivenvar.preview.HTMLPreviewPane;
-import com.scrivenvar.processors.Processor;
-import com.scrivenvar.processors.ProcessorFactory;
-import com.scrivenvar.service.Options;
-import com.scrivenvar.service.Snitch;
-import com.scrivenvar.service.events.Notifier;
-import com.scrivenvar.util.Action;
-import com.scrivenvar.util.ActionUtils;
-import javafx.application.Platform;
-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.geometry.Pos;
-import javafx.scene.Node;
-import javafx.scene.Scene;
-import javafx.scene.control.*;
-import javafx.scene.control.Alert.AlertType;
-import javafx.scene.image.Image;
-import javafx.scene.image.ImageView;
-import javafx.scene.input.KeyEvent;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.layout.VBox;
-import javafx.scene.text.Text;
-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.io.IOException;
-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.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 Notifier mNotifier = Services.load( Notifier.class );
-
- private final Scene mScene;
- private final StatusBar mStatusBar;
- private final Text mLineNumberText;
- private final TextField mFindTextField;
-
- /**
- * Default definition source is empty.
- */
- private DefinitionSource mDefinitionSource = new EmptyDefinitionSource();
- private final DefinitionPane mDefinitionPane = new DefinitionPane();
- private final HTMLPreviewPane mPreviewPane = new HTMLPreviewPane();
- private FileEditorTabPane fileEditorPane;
-
- /**
- * Prevents re-instantiation of processing classes.
- */
- private Map<FileEditorTab, Processor<String>> processors;
-
- /**
- * Listens on the definition pane for double-click events.
- */
- private VariableNameInjector variableNameInjector;
-
- 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 file editor tab pane to receive an open definition source event.
- */
- private void initDefinitionListener() {
- getFileEditorPane().onOpenDefinitionFileProperty().addListener(
- ( final ObservableValue<? extends Path> file,
- final Path oldPath, final Path newPath ) -> {
-
- // Indirectly refresh the resolved map.
- setProcessors( null );
- openDefinition( newPath );
-
- try {
- getSnitch().ignore( oldPath );
- getSnitch().listen( newPath );
- } catch( final IOException ex ) {
- error( ex );
- }
-
- // Will create new processors and therefore a new resolved map.
- refreshSelectedTab( getActiveFileEditor() );
- }
- );
- }
-
- /**
- * 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() {
- restoreDefinitionSource();
- 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 );
- }
-
- 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 {
- getNotifier().clear();
- processor.processChain( tab.getEditorText() );
- } catch( final Exception ex ) {
- error( ex );
- }
- }
-
- /**
- * 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 getDefinitionSource().getResolvedMap();
- }
-
- /**
- * Called when a definition source is opened.
- *
- * @param path Path to the definition source that was opened.
- */
- private void openDefinition( final Path path ) {
- try {
- final DefinitionSource ds = createDefinitionSource( path.toString() );
- setDefinitionSource( ds );
- storeDefinitionSource();
- getDefinitionPane().update( getDefinitionSource() );
- } catch( final Exception e ) {
- error( e );
- }
- }
-
- private void restoreDefinitionSource() {
- final Preferences preferences = getPreferences();
- final String source = preferences.get( PERSIST_DEFINITION_SOURCE, "" );
-
- openDefinition( new File( source ).toPath() );
- }
-
- private void storeDefinitionSource() {
- final Preferences preferences = getPreferences();
- final DefinitionSource ds = getDefinitionSource();
-
- preferences.put( PERSIST_DEFINITION_SOURCE, ds.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 ) {
- final Path path = (Path) value;
- final FileTypePredicate predicate
- = new FileTypePredicate( GLOB_DEFINITION_EXTENSIONS );
-
- // Reload definitions.
- if( predicate.test( path.toFile() ) ) {
- updateDefinitionSource( 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();
- refreshSelectedTab( getActiveFileEditor() );
- }
- );
- }
-
- /**
- * Reloads the definition source from the given path.
- *
- * @param path The path containing new definition information.
- */
- private void updateDefinitionSource( final Path path ) {
- Platform.runLater( () -> openDefinition( path ) );
- }
-
- /**
- * 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() {
- 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 void setProcessors(
- final Map<FileEditorTab, Processor<String>> map ) {
- this.processors = map;
- }
-
- private Map<FileEditorTab, Processor<String>> getProcessors() {
- if( this.processors == null ) {
- setProcessors( new HashMap<>() );
- }
-
- return this.processors;
- }
-
- 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 this.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 createDefinitionSource( final String path ) {
- DefinitionSource ds;
-
- try {
- ds = createDefinitionFactory().createDefinitionSource( path );
-
- if( ds instanceof FileDefinitionSource ) {
- try {
- getNotifier().notify( ds.getError() );
- getSnitch().listen( ((FileDefinitionSource) ds).getPath() );
- } catch( final Exception ex ) {
- error( ex );
- }
- }
- } catch( final Exception ex ) {
- ds = new EmptyDefinitionSource();
- error( ex );
- }
-
- return ds;
- }
-
- 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 );
-
- return new Scene( borderPane );
- }
-
- private Text createLineNumberText() {
- return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
- }
-
- private Node createMenuBar() {
- final BooleanBinding activeFileEditorIsNull =
- getFileEditorPane().activeFileEditorProperty()
- .isNull();
-
- // File actions
- final Action fileNewAction = new Action( get( "Main.menu.file.new" ),
- "Shortcut+N", FILE_ALT,
- e -> fileNew() );
- final Action fileOpenAction = new Action( get( "Main.menu.file.open" ),
- "Shortcut+O", FOLDER_OPEN_ALT,
- e -> fileOpen() );
- final Action fileCloseAction = new Action( get( "Main.menu.file.close" ),
- "Shortcut+W", null,
- e -> fileClose(),
- activeFileEditorIsNull );
- final Action fileCloseAllAction = new Action( get(
- "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(),
- activeFileEditorIsNull );
- final Action fileSaveAction = new Action( get( "Main.menu.file.save" ),
- "Shortcut+S", FLOPPY_ALT,
- e -> fileSave(),
- createActiveBooleanProperty(
- FileEditorTab::modifiedProperty )
- .not() );
- final Action fileSaveAsAction = new Action( Messages.get(
- "Main.menu.file.save_as" ), null, null, e -> fileSaveAs(),
- activeFileEditorIsNull );
- final Action fileSaveAllAction = new Action(
- get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null,
- e -> fileSaveAll(),
- Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
- final Action fileExitAction = new Action( get( "Main.menu.file.exit" ),
- null,
- null,
- e -> fileExit() );
-
- // Edit actions
- final Action editUndoAction = new Action( get( "Main.menu.edit.undo" ),
- "Shortcut+Z", UNDO,
- e -> getActiveEditor().undo(),
- createActiveBooleanProperty(
- FileEditorTab::canUndoProperty )
- .not() );
- final Action editRedoAction = new Action( get( "Main.menu.edit.redo" ),
- "Shortcut+Y", REPEAT,
- e -> getActiveEditor().redo(),
- createActiveBooleanProperty(
- FileEditorTab::canRedoProperty )
- .not() );
- final Action editFindAction = new Action( Messages.get(
- "Main.menu.edit.find" ), "Ctrl+F", SEARCH,
- e -> find(),
- activeFileEditorIsNull );
- final Action editFindNextAction = new Action( Messages.get(
- "Main.menu.edit.find.next" ), "F3", null,
- e -> findNext(),
- activeFileEditorIsNull );
-
- // Insert actions
- final Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ),
- "Shortcut+B", BOLD,
- e -> getActiveEditor().surroundSelection(
- "**", "**" ),
- activeFileEditorIsNull );
- final Action insertItalicAction = new Action(
- get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
- e -> getActiveEditor().surroundSelection( "*", "*" ),
- activeFileEditorIsNull );
- final Action insertSuperscriptAction = new Action( get(
- "Main.menu.insert.superscript" ), "Shortcut+[", SUPERSCRIPT,
- e -> getActiveEditor().surroundSelection(
- "^", "^" ),
- activeFileEditorIsNull );
- final Action insertSubscriptAction = new Action( get(
- "Main.menu.insert.subscript" ), "Shortcut+]", SUBSCRIPT,
- e -> getActiveEditor().surroundSelection(
- "~", "~" ),
- activeFileEditorIsNull );
- final Action insertStrikethroughAction = new Action( get(
- "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
- e -> getActiveEditor().surroundSelection(
- "~~", "~~" ),
- activeFileEditorIsNull );
- final Action insertBlockquoteAction = new Action( get(
- "Main.menu.insert.blockquote" ),
- "Ctrl+Q",
- QUOTE_LEFT,
- // not Shortcut+Q
- // because of conflict
- // on Mac
- e -> getActiveEditor().surroundSelection(
- "\n\n> ", "" ),
- activeFileEditorIsNull );
- final Action insertCodeAction = new Action( get( "Main.menu.insert.code" ),
- "Shortcut+K", CODE,
- e -> getActiveEditor().surroundSelection(
- "`", "`" ),
- activeFileEditorIsNull );
- final Action insertFencedCodeBlockAction = new Action( get(
- "Main.menu.insert.fenced_code_block" ),
- "Shortcut+Shift+K",
- FILE_CODE_ALT,
- e -> getActiveEditor()
- .surroundSelection(
- "\n\n```\n",
- "\n```\n\n",
- get(
- "Main.menu.insert.fenced_code_block.prompt" ) ),
- activeFileEditorIsNull );
-
- final Action insertLinkAction = new Action( get( "Main.menu.insert.link" ),
- "Shortcut+L", LINK,
- e -> getActiveEditor().insertLink(),
- activeFileEditorIsNull );
- final Action insertImageAction = new Action( get( "Main.menu.insert" +
- ".image" ),
- "Shortcut+G", PICTURE_ALT,
- e -> getActiveEditor().insertImage(),
- activeFileEditorIsNull );
-
- final Action[] headers = new Action[ 6 ];
-
- // Insert header actions (H1 ... H6)
- for( int i = 1; i <= 6; i++ ) {
- final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
- final String markup = String.format( "%n%n%s ", hashes );
- final String text = get( "Main.menu.insert.header_" + i );
- final String accelerator = "Shortcut+" + i;
- final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
-
- headers[ i - 1 ] = new Action( text, accelerator, HEADER,
- e -> getActiveEditor().surroundSelection(
- markup, "", prompt ),
- activeFileEditorIsNull );
- }
-
- final Action insertUnorderedListAction = new Action(
- get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
- e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
- activeFileEditorIsNull );
- final Action insertOrderedListAction = new Action(
- get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
- e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
- activeFileEditorIsNull );
- final Action insertHorizontalRuleAction = new Action(
- get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
- e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
- activeFileEditorIsNull );
-
- // R actions
- final Action mRScriptAction = new Action(
- get( "Main.menu.r.script" ), null, null, e -> rScript() );
-
- final Action mRDirectoryAction = new Action(
- get( "Main.menu.r.directory" ), null, null, e -> rDirectory() );
-
- // Help actions
- final Action helpAboutAction = new Action(
- get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
-
- //---- MenuBar ----
- final Menu fileMenu = ActionUtils.createMenu(
- get( "Main.menu.file" ),
- fileNewAction,
- fileOpenAction,
- null,
- fileCloseAction,
- fileCloseAllAction,
- null,
- fileSaveAction,
- fileSaveAsAction,
- fileSaveAllAction,
- null,
- fileExitAction );
-
- final Menu editMenu = ActionUtils.createMenu(
- get( "Main.menu.edit" ),
- editUndoAction,
- editRedoAction,
- editFindAction,
- editFindNextAction );
-
- final Menu insertMenu = ActionUtils.createMenu(
- get( "Main.menu.insert" ),
- insertBoldAction,
- insertItalicAction,
- insertSuperscriptAction,
- insertSubscriptAction,
- insertStrikethroughAction,
- insertBlockquoteAction,
- insertCodeAction,
- insertFencedCodeBlockAction,
- null,
- insertLinkAction,
- insertImageAction,
- null,
- headers[ 0 ],
- headers[ 1 ],
- headers[ 2 ],
- headers[ 3 ],
- headers[ 4 ],
- headers[ 5 ],
- null,
- insertUnorderedListAction,
- insertOrderedListAction,
- insertHorizontalRuleAction );
-
- final Menu rMenu = ActionUtils.createMenu(
- get( "Main.menu.r" ),
- mRScriptAction,
- mRDirectoryAction );
-
- final Menu helpMenu = ActionUtils.createMenu(
- get( "Main.menu.help" ),
- helpAboutAction );
-
- final MenuBar menuBar = new MenuBar(
- fileMenu,
- editMenu,
- insertMenu,
- rMenu,
- helpMenu );
-
- //---- ToolBar ----
- final ToolBar toolBar = ActionUtils.createToolBar(
- fileNewAction,
- fileOpenAction,
- fileSaveAction,
- null,
- editUndoAction,
- editRedoAction,
- null,
- insertBoldAction,
- insertItalicAction,
- insertSuperscriptAction,
- insertSubscriptAction,
- insertBlockquoteAction,
- insertCodeAction,
- insertFencedCodeBlockAction,
- null,
- insertLinkAction,
- insertImageAction,
- null,
- headers[ 0 ],
- null,
- insertUnorderedListAction,
- insertOrderedListAction );
-
- return new VBox( menuBar, toolBar );
- }
-
- /**
- * 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;
- }
-
- private void initLayout() {
- final Scene appScene = getScene();
-
- appScene.getStylesheets().add( STYLESHEET_SCENE );
-
- // TODO: Apply an XML syntax highlighting for XML files.
-// appScene.getStylesheets().add( STYLESHEET_XML );
- appScene.windowProperty().addListener(
- ( observable, oldWindow, newWindow ) ->
- newWindow.setOnCloseRequest(
- e -> {
- if( !getFileEditorPane().closeAllEditors() ) {
- e.consume();
- }
- }
- )
- );
- }
-
- private void putPreference( final String key, final String value ) {
- try {
- getPreferences().put( key, value );
- } catch( final Exception ex ) {
- getNotifier().notify( ex );
- }
+import com.scrivenvar.definition.yaml.YamlDefinitionSource;
+import com.scrivenvar.dialogs.RScriptDialog;
+import com.scrivenvar.editors.EditorPane;
+import com.scrivenvar.editors.VariableNameInjector;
+import com.scrivenvar.editors.markdown.MarkdownEditorPane;
+import com.scrivenvar.predicates.files.FileTypePredicate;
+import com.scrivenvar.preview.HTMLPreviewPane;
+import com.scrivenvar.processors.Processor;
+import com.scrivenvar.processors.ProcessorFactory;
+import com.scrivenvar.service.Options;
+import com.scrivenvar.service.Settings;
+import com.scrivenvar.service.Snitch;
+import com.scrivenvar.service.events.Notifier;
+import com.scrivenvar.util.Action;
+import com.scrivenvar.util.ActionUtils;
+import javafx.application.Platform;
+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.event.EventHandler;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Text;
+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.io.IOException;
+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.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 Map<FileEditorTab, Processor<String>> processors;
+
+ /**
+ * Listens on the definition pane for double-click events.
+ */
+ private VariableNameInjector variableNameInjector;
+
+
+ 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.
+ setProcessors( null );
+ openDefinition( newPath );
+
+ try {
+ getSnitch().ignore( oldPath );
+ getSnitch().listen( newPath );
+ } catch( final IOException ex ) {
+ error( ex );
+ }
+
+ // Will create new processors and therefore a new resolved map.
+ refreshSelectedTab( getActiveFileEditor() );
+ }
+ );
+ }
+
+ /**
+ * 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 );
+ }
+
+ 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 {
+ getNotifier().clear();
+ processor.processChain( tab.getEditorText() );
+ } catch( final Exception ex ) {
+ error( ex );
+ }
+ }
+
+ /**
+ * 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 getDefinitionSource().getResolvedMap();
+ }
+
+ final EventHandler<TreeItem.TreeModificationEvent<Event>> mHandler =
+ event -> exportDefinitionData( getDefinitionFilename() );
+
+ /**
+ * Called when a definition source is opened.
+ *
+ * @param path Path to the definition source that was opened.
+ */
+ private void openDefinition( final Path path ) {
+ try {
+
+ final DefinitionSource ds = createDefinitionSource( path.toString() );
+ setDefinitionSource( ds );
+ storeDefinitionSourceFilename();
+ getDefinitionPane().update( getDefinitionSource() );
+ getDefinitionPane().addTreeChangeHandler( mHandler );
+ } catch( final Exception e ) {
+ error( e );
+ }
+ }
+
+ private void exportDefinitionData( final Path path ) {
+ getDefinitionSource().export( path );
+ }
+
+ private Path getDefinitionFilename() {
+ final Preferences preferences = getPreferences();
+ final String defaultFilename = getSetting(
+ "file.definition.default", "variables.yaml" );
+ String source = preferences.get(
+ PERSIST_DEFINITION_SOURCE, defaultFilename );
+
+ if( source.isBlank() ) {
+ source = defaultFilename;
+ }
+
+ return new File( source ).toPath();
+ }
+
+ private void restoreDefinitionPane() {
+ openDefinition( getDefinitionFilename() );
+ }
+
+ private void storeDefinitionSourceFilename() {
+ final Preferences preferences = getPreferences();
+ final DefinitionSource ds = getDefinitionSource();
+ final String source = ds.toString();
+
+ preferences.put( PERSIST_DEFINITION_SOURCE, source );
+ }
+
+ /**
+ * 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 ) {
+ final Path path = (Path) value;
+ final FileTypePredicate predicate
+ = new FileTypePredicate( GLOB_DEFINITION_EXTENSIONS );
+
+ // Reload definitions.
+ if( predicate.test( path.toFile() ) ) {
+ updateDefinitionSource( 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();
+ refreshSelectedTab( getActiveFileEditor() );
+ }
+ );
+ }
+
+ /**
+ * Reloads the definition source from the given path.
+ *
+ * @param path The path containing new definition information.
+ */
+ private void updateDefinitionSource( final Path path ) {
+ Platform.runLater( () -> openDefinition( path ) );
+ }
+
+ /**
+ * 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() {
+ 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 void setProcessors(
+ final Map<FileEditorTab, Processor<String>> map ) {
+ this.processors = map;
+ }
+
+ private Map<FileEditorTab, Processor<String>> getProcessors() {
+ if( this.processors == null ) {
+ setProcessors( new HashMap<>() );
+ }
+
+ return this.processors;
+ }
+
+ 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 this.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( getDefinitionFilename() );
+ }
+
+ private DefinitionSource createDefinitionSource( final String path ) {
+ DefinitionSource ds;
+
+ try {
+ ds = createDefinitionFactory().createDefinitionSource( path );
+
+ if( ds instanceof FileDefinitionSource ) {
+ try {
+ getNotifier().notify( ds.getError() );
+ getSnitch().listen( ((FileDefinitionSource) ds).getPath() );
+ } catch( final Exception ex ) {
+ error( ex );
+ }
+ }
+ } catch( final Exception ex ) {
+ ds = createDefaultDefinitionSource();
+ error( ex );
+ }
+
+ return ds;
+ }
+
+ 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 );
+
+ return new Scene( borderPane );
+ }
+
+ private Text createLineNumberText() {
+ return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
+ }
+
+ private Node createMenuBar() {
+ final BooleanBinding activeFileEditorIsNull =
+ getFileEditorPane().activeFileEditorProperty()
+ .isNull();
+
+ // File actions
+ final Action fileNewAction = new Action( get( "Main.menu.file.new" ),
+ "Shortcut+N", FILE_ALT,
+ e -> fileNew() );
+ final Action fileOpenAction = new Action( get( "Main.menu.file.open" ),
+ "Shortcut+O", FOLDER_OPEN_ALT,
+ e -> fileOpen() );
+ final Action fileCloseAction = new Action( get( "Main.menu.file.close" ),
+ "Shortcut+W", null,
+ e -> fileClose(),
+ activeFileEditorIsNull );
+ final Action fileCloseAllAction = new Action( get(
+ "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(),
+ activeFileEditorIsNull );
+ final Action fileSaveAction = new Action( get( "Main.menu.file.save" ),
+ "Shortcut+S", FLOPPY_ALT,
+ e -> fileSave(),
+ createActiveBooleanProperty(
+ FileEditorTab::modifiedProperty )
+ .not() );
+ final Action fileSaveAsAction = new Action( Messages.get(
+ "Main.menu.file.save_as" ), null, null, e -> fileSaveAs(),
+ activeFileEditorIsNull );
+ final Action fileSaveAllAction = new Action(
+ get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null,
+ e -> fileSaveAll(),
+ Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
+ final Action fileExitAction = new Action( get( "Main.menu.file.exit" ),
+ null,
+ null,
+ e -> fileExit() );
+
+ // Edit actions
+ final Action editUndoAction = new Action( get( "Main.menu.edit.undo" ),
+ "Shortcut+Z", UNDO,
+ e -> getActiveEditor().undo(),
+ createActiveBooleanProperty(
+ FileEditorTab::canUndoProperty )
+ .not() );
+ final Action editRedoAction = new Action( get( "Main.menu.edit.redo" ),
+ "Shortcut+Y", REPEAT,
+ e -> getActiveEditor().redo(),
+ createActiveBooleanProperty(
+ FileEditorTab::canRedoProperty )
+ .not() );
+ final Action editFindAction = new Action( Messages.get(
+ "Main.menu.edit.find" ), "Ctrl+F", SEARCH,
+ e -> find(),
+ activeFileEditorIsNull );
+ final Action editFindNextAction = new Action( Messages.get(
+ "Main.menu.edit.find.next" ), "F3", null,
+ e -> findNext(),
+ activeFileEditorIsNull );
+
+ // Insert actions
+ final Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ),
+ "Shortcut+B", BOLD,
+ e -> getActiveEditor().surroundSelection(
+ "**", "**" ),
+ activeFileEditorIsNull );
+ final Action insertItalicAction = new Action(
+ get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
+ e -> getActiveEditor().surroundSelection( "*", "*" ),
+ activeFileEditorIsNull );
+ final Action insertSuperscriptAction = new Action( get(
+ "Main.menu.insert.superscript" ), "Shortcut+[", SUPERSCRIPT,
+ e -> getActiveEditor().surroundSelection(
+ "^", "^" ),
+ activeFileEditorIsNull );
+ final Action insertSubscriptAction = new Action( get(
+ "Main.menu.insert.subscript" ), "Shortcut+]", SUBSCRIPT,
+ e -> getActiveEditor().surroundSelection(
+ "~", "~" ),
+ activeFileEditorIsNull );
+ final Action insertStrikethroughAction = new Action( get(
+ "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
+ e -> getActiveEditor().surroundSelection(
+ "~~", "~~" ),
+ activeFileEditorIsNull );
+ final Action insertBlockquoteAction = new Action( get(
+ "Main.menu.insert.blockquote" ),
+ "Ctrl+Q",
+ QUOTE_LEFT,
+ // not Shortcut+Q
+ // because of conflict
+ // on Mac
+ e -> getActiveEditor().surroundSelection(
+ "\n\n> ", "" ),
+ activeFileEditorIsNull );
+ final Action insertCodeAction = new Action( get( "Main.menu.insert.code" ),
+ "Shortcut+K", CODE,
+ e -> getActiveEditor().surroundSelection(
+ "`", "`" ),
+ activeFileEditorIsNull );
+ final Action insertFencedCodeBlockAction = new Action( get(
+ "Main.menu.insert.fenced_code_block" ),
+ "Shortcut+Shift+K",
+ FILE_CODE_ALT,
+ e -> getActiveEditor()
+ .surroundSelection(
+ "\n\n```\n",
+ "\n```\n\n",
+ get(
+ "Main.menu.insert.fenced_code_block.prompt" ) ),
+ activeFileEditorIsNull );
+
+ final Action insertLinkAction = new Action( get( "Main.menu.insert.link" ),
+ "Shortcut+L", LINK,
+ e -> getActiveEditor().insertLink(),
+ activeFileEditorIsNull );
+ final Action insertImageAction = new Action( get( "Main.menu.insert" +
+ ".image" ),
+ "Shortcut+G", PICTURE_ALT,
+ e -> getActiveEditor().insertImage(),
+ activeFileEditorIsNull );
+
+ final Action[] headers = new Action[ 6 ];
+
+ // Insert header actions (H1 ... H6)
+ for( int i = 1; i <= 6; i++ ) {
+ final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
+ final String markup = String.format( "%n%n%s ", hashes );
+ final String text = get( "Main.menu.insert.header_" + i );
+ final String accelerator = "Shortcut+" + i;
+ final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
+
+ headers[ i - 1 ] = new Action( text, accelerator, HEADER,
+ e -> getActiveEditor().surroundSelection(
+ markup, "", prompt ),
+ activeFileEditorIsNull );
+ }
+
+ final Action insertUnorderedListAction = new Action(
+ get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
+ e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
+ activeFileEditorIsNull );
+ final Action insertOrderedListAction = new Action(
+ get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
+ e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
+ activeFileEditorIsNull );
+ final Action insertHorizontalRuleAction = new Action(
+ get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
+ e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
+ activeFileEditorIsNull );
+
+ // R actions
+ final Action mRScriptAction = new Action(
+ get( "Main.menu.r.script" ), null, null, e -> rScript() );
+
+ final Action mRDirectoryAction = new Action(
+ get( "Main.menu.r.directory" ), null, null, e -> rDirectory() );
+
+ // Help actions
+ final Action helpAboutAction = new Action(
+ get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
+
+ //---- MenuBar ----
+ final Menu fileMenu = ActionUtils.createMenu(
+ get( "Main.menu.file" ),
+ fileNewAction,
+ fileOpenAction,
+ null,
+ fileCloseAction,
+ fileCloseAllAction,
+ null,
+ fileSaveAction,
+ fileSaveAsAction,
+ fileSaveAllAction,
+ null,
+ fileExitAction );
+
+ final Menu editMenu = ActionUtils.createMenu(
+ get( "Main.menu.edit" ),
+ editUndoAction,
+ editRedoAction,
+ editFindAction,
+ editFindNextAction );
+
+ final Menu insertMenu = ActionUtils.createMenu(
+ get( "Main.menu.insert" ),
+ insertBoldAction,
+ insertItalicAction,
+ insertSuperscriptAction,
+ insertSubscriptAction,
+ insertStrikethroughAction,
+ insertBlockquoteAction,
+ insertCodeAction,
+ insertFencedCodeBlockAction,
+ null,
+ insertLinkAction,
+ insertImageAction,
+ null,
+ headers[ 0 ],
+ headers[ 1 ],
+ headers[ 2 ],
+ headers[ 3 ],
+ headers[ 4 ],
+ headers[ 5 ],
+ null,
+ insertUnorderedListAction,
+ insertOrderedListAction,
+ insertHorizontalRuleAction );
+
+ final Menu rMenu = ActionUtils.createMenu(
+ get( "Main.menu.r" ),
+ mRScriptAction,
+ mRDirectoryAction );
+
+ final Menu helpMenu = ActionUtils.createMenu(
+ get( "Main.menu.help" ),
+ helpAboutAction );
+
+ final MenuBar menuBar = new MenuBar(
+ fileMenu,
+ editMenu,
+ insertMenu,
+ rMenu,
+ helpMenu );
+
+ //---- ToolBar ----
+ final ToolBar toolBar = ActionUtils.createToolBar(
+ fileNewAction,
+ fileOpenAction,
+ fileSaveAction,
+ null,
+ editUndoAction,
+ editRedoAction,
+ null,
+ insertBoldAction,
+ insertItalicAction,
+ insertSuperscriptAction,
+ insertSubscriptAction,
+ insertBlockquoteAction,
+ insertCodeAction,
+ insertFencedCodeBlockAction,
+ null,
+ insertLinkAction,
+ insertImageAction,
+ null,
+ headers[ 0 ],
+ null,
+ insertUnorderedListAction,
+ insertOrderedListAction );
+
+ return new VBox( menuBar, toolBar );
+ }
+
+ /**
+ * 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;
+ }
+
+ private void initLayout() {
+ final Scene appScene = getScene();
+
+ appScene.getStylesheets().add( STYLESHEET_SCENE );
+
+ // TODO: Apply an XML syntax highlighting for XML files.
+// appScene.getStylesheets().add( STYLESHEET_XML );
+ appScene.windowProperty().addListener(
+ ( observable, oldWindow, newWindow ) ->
+ newWindow.setOnCloseRequest(
+ e -> {
+ if( !getFileEditorPane().closeAllEditors() ) {
+ e.consume();
+ }
+ }
+ )
+ );
+ }
+
+ private void putPreference( final String key, final String value ) {
+ try {
+ getPreferences().put( key, value );
+ } catch( final Exception ex ) {
+ getNotifier().notify( ex );
+ }
+ }
+
+ /**
+ * Returns the value for a key from the settings properties file.
+ *
+ * @param key Key within the settings properties file to find.
+ * @param value Default value to return if the key is not found.
+ * @return The value for the given key from the settings file, or the
+ * given {@code value} if no key found.
+ */
+ private String getSetting( final String key, final String value ) {
+ return mSettings.getSetting( key, value );
}
}
src/main/java/com/scrivenvar/definition/DefinitionFactory.java
import com.scrivenvar.AbstractFileFactory;
import com.scrivenvar.FileType;
-import com.scrivenvar.definition.yaml.YamlFileDefinitionSource;
+import com.scrivenvar.definition.yaml.YamlDefinitionSource;
import java.io.File;
private DefinitionSource createFileDefinitionSource(
final FileType filetype, final Path path ) {
+ assert filetype != null;
+ assert path !=null;
- return filetype == YAML
- ? new YamlFileDefinitionSource( path )
- : new EmptyDefinitionSource();
+ if( filetype == YAML ) {
+ return new YamlDefinitionSource( path );
+ }
+
+ throw new IllegalArgumentException( filetype.toString() );
}
src/main/java/com/scrivenvar/definition/DefinitionPane.java
import com.scrivenvar.AbstractPane;
-import javafx.beans.value.ObservableValue;
-import javafx.collections.ObservableList;
-import javafx.scene.Node;
-import javafx.scene.control.*;
-import javafx.scene.control.cell.TextFieldTreeCell;
-import javafx.scene.input.KeyCode;
-import javafx.scene.input.KeyEvent;
-import javafx.util.StringConverter;
-
-import java.util.List;
-
-import static com.scrivenvar.Messages.get;
-
-/**
- * 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 = ":;,.!?-/\\¡¿";
-
- private final TreeView<String> mTreeView = new TreeView<>();
-
- /**
- * Constructs a definition pane with a given tree view root.
- */
- public DefinitionPane() {
- initTreeView();
- }
-
- /**
- * 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> treeItem = treeAdapter.adapt(
- get( "Pane.definition.node.root.title" )
- );
-
- update( treeItem );
- }
-
- /**
- * Changes the root of the {@link TreeView} to the root of the given
- * {@link TreeView}.
- *
- * @param treeItem The new hierarchy of key/value pairs to replace the
- * existing hierarchy.
- */
- private void update( final TreeItem<String> treeItem ) {
- assert treeItem != null;
-
- getTreeView().setRoot( treeItem );
- }
-
- /**
- * 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 ) {
- clearSelection();
- selectItem( getTreeView().getRow( item ) );
- }
-
- private void clearSelection() {
- getSelectionModel().clearSelection();
- }
-
- private void selectItem( final int row ) {
- getSelectionModel().select( row );
- }
-
- /**
- * 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( ObservableList<TreeItem<T>> nodes ) {
- for( final TreeItem<T> node : nodes ) {
- node.setExpanded( false );
- collapse( node.getChildren() );
- }
- }
-
- /**
- * Returns the root node to the tree view.
- *
- * @return getTreeView()
- */
- public Node getNode() {
- return getTreeView();
- }
-
- private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
- return getTreeView().getSelectionModel();
- }
-
- private void initTreeView() {
- final TreeView<String> treeView = getTreeView();
-
- treeView.setContextMenu( createContextMenu() );
- treeView.setEditable( true );
- treeView.setCellFactory( cell -> createTreeCell() );
- treeView.addEventFilter( KeyEvent.KEY_PRESSED, this::keyEventFilter );
- setSelectionMode( SelectionMode.MULTIPLE );
- }
-
- /**
- * Returns the root of the tree.
- *
- * @return The first node added to the YAML definition tree.
- */
- private VariableTreeItem<String> getTreeRoot() {
- final TreeItem<String> root = getTreeView().getRoot();
-
- return root instanceof VariableTreeItem ?
- (VariableTreeItem<String>) root : new VariableTreeItem<>( "root" );
- }
-
- private ContextMenu createContextMenu() {
- final ContextMenu menu = new ContextMenu();
- final ObservableList<MenuItem> items = menu.getItems();
-
- addMenuItem( items, "Definition.menu.create" ).setOnAction(
- e -> getSelectedItem().getChildren().add( createTreeItem() )
- );
-
- addMenuItem( items, "Definition.menu.rename" ).setOnAction(
- e -> getTreeView().edit( getSelectedItem() )
- );
-
- addMenuItem( items, "Definition.menu.remove" ).setOnAction(
- e -> {
- final TreeItem<String> c = getSelectedItem();
- getSiblings( c ).remove( c );
- }
- );
-
- return menu;
- }
-
- private ObservableList<TreeItem<String>> getSiblings(
- final TreeItem<String> item ) {
- final TreeItem<String> root = getTreeView().getRoot();
- final TreeItem<String> parent =
- (item == null || item == root) ? root : item.getParent();
-
- return parent.getChildren();
- }
-
- /**
- * 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 );
- 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;
- }
- };
- }
-
- private void keyEventFilter( final KeyEvent event ) {
- if( event.getCode() == KeyCode.ENTER ) {
- final ObservableValue<TreeItem<String>> property =
- getTreeView().editingItemProperty();
-
- // Consume ENTER presses when not editing a definition value.
- if( property.getValue() == null ) {
- event.consume();
- }
- }
- }
-
- private TreeItem<String> getSelectedItem() {
- final TreeItem<String> item =
- getTreeView().getSelectionModel().getSelectedItem();
- return item == null ? getTreeView().getRoot() : item;
- }
-
- /**
- * Delegates to {@link #getSelectionModel()}.
- *
- * @param mode The new selection mode (to enable multiple select).
- */
- @SuppressWarnings("SameParameterValue")
- private void setSelectionMode( final SelectionMode mode ) {
- getSelectionModel().setSelectionMode( mode );
- }
-
- /**
- * Returns the tree view that contains the YAML definition hierarchy.
- *
- * @return A non-null instance.
- */
- private TreeView<String> getTreeView() {
- return mTreeView;
- }
+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.LinkedList;
+import java.util.List;
+
+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<>();
+
+ /**
+ * Constructs a definition pane with a given tree view root.
+ */
+ public DefinitionPane() {
+ getTreeView().setEditable( true );
+ getTreeView().setCellFactory( cell -> createTreeCell() );
+ getTreeView().setContextMenu( createContextMenu() );
+ getTreeView().addEventFilter( KEY_PRESSED, this::keyEventFilter );
+ 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 );
+ }
+
+ /**
+ * 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.
+ *
+ * @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 );
+ }
+
+ /**
+ * 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> ti : getSelectedItems() ) {
+ ti.getParent().getChildren().remove( ti );
+ }
+ }
+
+ /**
+ * 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).
+ */
+ private void addItem() {
+ final TreeItem<String> treeItem = createTreeItem();
+ getSelectedItem().getChildren().add( treeItem );
+ expand( treeItem );
+ select( treeItem );
+ }
+
+ 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;
+ }
+
+ private void keyEventFilter( final KeyEvent event ) {
+ switch( event.getCode() ) {
+ case ENTER:
+ if( !isEditingTreeItem() ) {
+ expand( getSelectedItem() );
+ event.consume();
+ }
+
+ break;
+
+ case DELETE:
+ deleteSelectedItems();
+ break;
+
+ case INSERT:
+ addItem();
+ break;
+
+ case R:
+ if( event.isControlDown() ) {
+ editSelectedItem();
+ }
+
+ break;
+ }
+ }
+
+ /**
+ * 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 );
+ requestFocus();
+ select( getTreeItem() );
+ }
+ };
+ }
+
+ 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 YAML 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();
+ }
+
+ /**
+ * Returns the root of the tree.
+ *
+ * @return The first node added to the YAML definition tree.
+ */
+ private VariableTreeItem<String> getTreeRoot() {
+ final TreeItem<String> root = getTreeView().getRoot();
+
+ return root instanceof VariableTreeItem ?
+ (VariableTreeItem<String>) root : new VariableTreeItem<>( "root" );
+ }
+
+ private ObservableList<TreeItem<String>> getSiblings(
+ final TreeItem<String> item ) {
+ final TreeItem<String> root = getTreeView().getRoot();
+ final TreeItem<String> parent =
+ (item == null || item == root) ? root : item.getParent();
+
+ return parent.getChildren();
+ }
+
+ private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
+ return getTreeView().getSelectionModel();
+ }
+
+ /**
+ * Returns a copy of all the selected items.
+ *
+ * @return A list, possibly empty, containing all selected items in the
+ * {@link TreeView}.
+ */
+ private List<TreeItem<String>> getSelectedItems() {
+ return new LinkedList<>( getSelectionModel().getSelectedItems() );
+ }
+
+ private TreeItem<String> getSelectedItem() {
+ final TreeItem<String> item = getSelectionModel().getSelectedItem();
+ return item == null ? getTreeView().getRoot() : item;
+ }
+
}
src/main/java/com/scrivenvar/definition/DefinitionSource.java
package com.scrivenvar.definition;
+import java.nio.file.Path;
import java.util.Map;
/**
- * Represents behaviours for reading and writing variable definitions. This
+ * Represents behaviours for reading and writing string definitions. This
* class cannot have any direct hooks into the user interface, as it defines
* entry points into the definition data model loaded into an object
*/
Map<String, String> getResolvedMap();
+
+ /**
+ * Exports the data source to the given path. Performs no operation by
+ * default.
+ *
+ * @param path The path to write the interpolated string definitions.
+ */
+ default void export( final Path path ) {
+ }
/**
src/main/java/com/scrivenvar/definition/EmptyDefinitionSource.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.definition;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Creates a definition source that has no information to load or save.
- *
- * @author White Magic Software, Ltd.
- */
-public class EmptyDefinitionSource implements DefinitionSource {
-
- public EmptyDefinitionSource() {
- }
-
- @Override
- public TreeAdapter getTreeAdapter() {
- return new EmptyTreeAdapter();
- }
-
- @Override
- public Map<String, String> getResolvedMap() {
- return new HashMap<>();
- }
-
- /**
- * Prevent an {@link EmptyDefinitionSource} from being saved literally as its
- * memory reference (the default value returned by {@link Object#toString()}).
- *
- * @return Empty string.
- */
- @Override
- public String toString() {
- return "";
- }
-}
src/main/java/com/scrivenvar/definition/EmptyTreeAdapter.java
-package com.scrivenvar.definition;
-
-import javafx.scene.control.TreeItem;
-
-/**
- * Facilitates adapting empty documents into a single node object model.
- */
-public class EmptyTreeAdapter implements TreeAdapter {
- @Override
- public TreeItem<String> adapt( String root ) {
- return new TreeItem<>( root );
- }
-}
src/main/java/com/scrivenvar/definition/VariableTreeItem.java
import java.util.Stack;
-import static com.scrivenvar.definition.FindMode.*;
+import static com.scrivenvar.definition.FindMode.CONTAINS;
+import static com.scrivenvar.definition.FindMode.STARTS_WITH;
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR;
-import static com.scrivenvar.editors.VariableNameInjector.DEFAULT_MAX_VAR_LENGTH;
import static java.text.Normalizer.Form.NFD;
/**
* Provides behaviour afforded to variable names and their corresponding value.
*
* @param <T> The type of TreeItem (usually String).
* @author White Magic Software, Ltd.
*/
public class VariableTreeItem<T> extends TreeItem<T> {
+ public static final int DEFAULT_MAX_VAR_LENGTH = 64;
/**
src/main/java/com/scrivenvar/definition/yaml/YamlDefinitionSource.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.definition.yaml;
+
+import com.scrivenvar.definition.FileDefinitionSource;
+import com.scrivenvar.definition.TreeAdapter;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+/**
+ * Represents a definition data source for YAML files.
+ *
+ * @author White Magic Software, Ltd.
+ */
+public class YamlDefinitionSource extends FileDefinitionSource {
+
+ private final YamlParser mYamlParser;
+ private final YamlTreeAdapter mYamlTreeAdapter;
+
+ /**
+ * Constructs a new YAML definition source, populated from the given file.
+ *
+ * @param path Path to the YAML definition file.
+ */
+ public YamlDefinitionSource( final Path path ) {
+ super( path );
+ mYamlParser = createYamlParser( path );
+ mYamlTreeAdapter = createTreeAdapter( mYamlParser );
+ }
+
+ @Override
+ public TreeAdapter getTreeAdapter() {
+ return mYamlTreeAdapter;
+ }
+
+ @Override
+ public Map<String, String> getResolvedMap() {
+ return getYamlParser().createResolvedMap();
+ }
+
+ @Override
+ public void export( final Path path ) {
+ System.out.println( "Export YAML definitions to: " + path.toString() );
+ }
+
+ @Override
+ public String getError() {
+ return getYamlParser().getError();
+ }
+
+ private YamlParser createYamlParser( final Path path ) {
+ return YamlParser.parse(path);
+ }
+
+ private YamlParser getYamlParser() {
+ return mYamlParser;
+ }
+
+ private YamlTreeAdapter createTreeAdapter( final YamlParser parser ) {
+ return new YamlTreeAdapter( parser );
+ }
+}
src/main/java/com/scrivenvar/definition/yaml/YamlFileDefinitionSource.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.definition.yaml;
-
-import com.scrivenvar.definition.FileDefinitionSource;
-import com.scrivenvar.definition.TreeAdapter;
-
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Map;
-
-/**
- * Represents a definition data source for YAML files.
- *
- * @author White Magic Software, Ltd.
- */
-public class YamlFileDefinitionSource extends FileDefinitionSource {
-
- private final YamlParser mYamlParser;
- private final YamlTreeAdapter mYamlTreeAdapter;
-
- /**
- * Constructs a new YAML definition source, populated from the given file.
- *
- * @param path Path to the YAML definition file.
- */
- public YamlFileDefinitionSource( final Path path ) {
- super( path );
- mYamlParser = createYamlParser( path );
- mYamlTreeAdapter = createTreeAdapter( mYamlParser );
- }
-
- @Override
- public TreeAdapter getTreeAdapter() {
- return mYamlTreeAdapter;
- }
-
- @Override
- public Map<String, String> getResolvedMap() {
- return getYamlParser().createResolvedMap();
- }
-
- @Override
- public String getError() {
- return getYamlParser().getError();
- }
-
- private YamlParser createYamlParser( final Path path ) {
- try( final InputStream in = Files.newInputStream( path ) ) {
- return new YamlParser( in );
- } catch( final Exception ex ) {
- throw new RuntimeException( ex );
- }
- }
-
- private YamlParser getYamlParser() {
- return mYamlParser;
- }
-
- private YamlTreeAdapter createTreeAdapter( final YamlParser parser ) {
- return new YamlTreeAdapter( parser );
- }
-}
src/main/java/com/scrivenvar/definition/yaml/YamlParser.java
import org.yaml.snakeyaml.DumperOptions;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
private Map<String, String> references;
- public YamlParser( final InputStream in ) {
- process( in );
+ /**
+ * Creates a new YamlParser instance that attempts to parse the given
+ * YAML document input stream.
+ *
+ * @param path Path to a file containing YAML data to parse. The file does
+ * not have to exist.
+ * @return A new instance of this class.
+ */
+ public static YamlParser parse( final Path path ) {
+ final YamlParser parser = new YamlParser();
+
+ try( final InputStream in = Files.newInputStream( path ) ) {
+ parser.process( in );
+ } catch( final Exception e ) {
+ // Ensure that a document root node exists by relying on the
+ // default failure condition when processing. This is required
+ // because the input stream could not be read.
+ parser.process( new ByteArrayInputStream( new byte[]{} ) );
+ }
+
+ return parser;
+ }
+
+ /**
+ * Prevent instantiation.
+ */
+ private YamlParser() {
}
src/main/java/com/scrivenvar/definition/yaml/resolvers/ResolverYAMLFactory.java
-/*
- * The MIT License
- *
- * Copyright 2017 White Magic Software, Ltd..
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-package com.scrivenvar.definition.yaml.resolvers;
-
-import com.fasterxml.jackson.core.io.IOContext;
-import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
-import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
-import com.scrivenvar.definition.yaml.YamlParser;
-import java.io.IOException;
-import java.io.Writer;
-
-/**
- * Responsible for producing YAML generators.
- *
- * @author White Magic Software, Ltd.
- */
-public final class ResolverYAMLFactory extends YAMLFactory {
-
- private static final long serialVersionUID = 1L;
-
- private YamlParser yamlParser;
-
- public ResolverYAMLFactory( final YamlParser yamlParser ) {
- setYamlParser( yamlParser );
- }
-
- @Override
- protected YAMLGenerator _createGenerator(
- final Writer out, final IOContext ctxt ) throws IOException {
-
- return new ResolverYAMLGenerator(
- getYamlParser(),
- ctxt, _generatorFeatures, _yamlGeneratorFeatures, _objectCodec,
- out, _version );
- }
-
- /**
- * Returns the YAML parser used when constructing this instance.
- *
- * @return A non-null instance.
- */
- private YamlParser getYamlParser() {
- return this.yamlParser;
- }
-
- /**
- * Sets the YAML parser used when constructing this instance.
- *
- * @param yamlParser A non-null instance.
- */
- private void setYamlParser( final YamlParser yamlParser ) {
- this.yamlParser = yamlParser;
- }
-}
src/main/java/com/scrivenvar/definition/yaml/resolvers/ResolverYAMLGenerator.java
-/*
- * Copyright 2020 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.definition.yaml.resolvers;
-
-import com.fasterxml.jackson.core.ObjectCodec;
-import com.fasterxml.jackson.core.io.IOContext;
-import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
-import com.scrivenvar.definition.yaml.YamlParser;
-import org.yaml.snakeyaml.DumperOptions;
-
-import java.io.IOException;
-import java.io.Writer;
-
-/**
- * Intercepts the string writing functionality to resolve the definition
- * value.
- */
-public class ResolverYAMLGenerator extends YAMLGenerator {
-
- private YamlParser yamlParser;
-
- public ResolverYAMLGenerator(
- final YamlParser yamlParser,
- final IOContext ctxt,
- final int jsonFeatures,
- final int yamlFeatures,
- final ObjectCodec codec,
- final Writer out,
- final DumperOptions.Version version ) throws IOException {
- super( ctxt, jsonFeatures, yamlFeatures, codec, out, version );
- setYamlParser( yamlParser );
- }
-
- @Override
- public void writeString( final String text ) throws IOException {
- final YamlParser parser = getYamlParser();
- super.writeString( parser.substitute( text ) );
- }
-
- private YamlParser getYamlParser() {
- return yamlParser;
- }
-
- private void setYamlParser( final YamlParser yamlParser ) {
- this.yamlParser = yamlParser;
- }
-}
src/main/java/com/scrivenvar/definition/yaml/resolvers/ResolverYamlFactory.java
+/*
+ * The MIT License
+ *
+ * Copyright 2017 White Magic Software, Ltd..
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.scrivenvar.definition.yaml.resolvers;
+
+import com.fasterxml.jackson.core.io.IOContext;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
+import com.scrivenvar.definition.yaml.YamlParser;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Responsible for producing YAML generators.
+ *
+ * @author White Magic Software, Ltd.
+ */
+public final class ResolverYamlFactory extends YAMLFactory {
+
+ private static final long serialVersionUID = 1L;
+
+ private YamlParser yamlParser;
+
+ public ResolverYamlFactory( final YamlParser yamlParser ) {
+ setYamlParser( yamlParser );
+ }
+
+ @Override
+ protected YAMLGenerator _createGenerator(
+ final Writer out, final IOContext ctxt ) throws IOException {
+
+ return new ResolverYamlGenerator(
+ getYamlParser(),
+ ctxt, _generatorFeatures, _yamlGeneratorFeatures, _objectCodec,
+ out, _version );
+ }
+
+ /**
+ * Returns the YAML parser used when constructing this instance.
+ *
+ * @return A non-null instance.
+ */
+ private YamlParser getYamlParser() {
+ return this.yamlParser;
+ }
+
+ /**
+ * Sets the YAML parser used when constructing this instance.
+ *
+ * @param yamlParser A non-null instance.
+ */
+ private void setYamlParser( final YamlParser yamlParser ) {
+ this.yamlParser = yamlParser;
+ }
+}
src/main/java/com/scrivenvar/definition/yaml/resolvers/ResolverYamlGenerator.java
+/*
+ * Copyright 2020 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.definition.yaml.resolvers;
+
+import com.fasterxml.jackson.core.ObjectCodec;
+import com.fasterxml.jackson.core.io.IOContext;
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
+import com.scrivenvar.definition.yaml.YamlParser;
+import org.yaml.snakeyaml.DumperOptions;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Intercepts the string writing functionality to resolve the definition
+ * value.
+ */
+public class ResolverYamlGenerator extends YAMLGenerator {
+
+ private YamlParser yamlParser;
+
+ public ResolverYamlGenerator(
+ final YamlParser yamlParser,
+ final IOContext ctxt,
+ final int jsonFeatures,
+ final int yamlFeatures,
+ final ObjectCodec codec,
+ final Writer out,
+ final DumperOptions.Version version ) throws IOException {
+ super( ctxt, jsonFeatures, yamlFeatures, codec, out, version );
+ setYamlParser( yamlParser );
+ }
+
+ @Override
+ public void writeString( final String text ) throws IOException {
+ final YamlParser parser = getYamlParser();
+ super.writeString( parser.substitute( text ) );
+ }
+
+ private YamlParser getYamlParser() {
+ return yamlParser;
+ }
+
+ private void setYamlParser( final YamlParser yamlParser ) {
+ this.yamlParser = yamlParser;
+ }
+}
src/main/java/com/scrivenvar/editors/VariableNameInjector.java
public final class VariableNameInjector {
- public static final int DEFAULT_MAX_VAR_LENGTH = 64;
-
/**
* Recipient of name injections.
addKeyboardListener(
keyPressed( SPACE, CONTROL_DOWN ),
- this::autoinsert );
+ this::autoinsert
+ );
}
src/main/java/com/scrivenvar/util/StageState.java
import java.util.prefs.Preferences;
+
import javafx.application.Platform;
import javafx.scene.shape.Rectangle;
public static final String K_PANE_SPLIT_PREVIEW = "pane.split.preview";
- private final Stage stage;
- private final Preferences state;
+ private final Stage mStage;
+ private final Preferences mState;
private Rectangle normalBounds;
private boolean runLaterPending;
public StageState( final Stage stage, final Preferences state ) {
- this.stage = stage;
- this.state = state;
+ mStage = stage;
+ mState = state;
restore();
stage.addEventHandler( WindowEvent.WINDOW_HIDING, e -> save() );
- stage.xProperty().addListener( (ob, o, n) -> boundsChanged() );
- stage.yProperty().addListener( (ob, o, n) -> boundsChanged() );
- stage.widthProperty().addListener( (ob, o, n) -> boundsChanged() );
- stage.heightProperty().addListener( (ob, o, n) -> boundsChanged() );
+ stage.xProperty().addListener( ( ob, o, n ) -> boundsChanged() );
+ stage.yProperty().addListener( ( ob, o, n ) -> boundsChanged() );
+ stage.widthProperty().addListener( ( ob, o, n ) -> boundsChanged() );
+ stage.heightProperty().addListener( ( ob, o, n ) -> boundsChanged() );
}
private void save() {
final Rectangle bounds = isNormalState() ? getStageBounds() : normalBounds;
-
+
if( bounds != null ) {
- state.putDouble( "windowX", bounds.getX() );
- state.putDouble( "windowY", bounds.getY() );
- state.putDouble( "windowWidth", bounds.getWidth() );
- state.putDouble( "windowHeight", bounds.getHeight() );
+ mState.putDouble( "windowX", bounds.getX() );
+ mState.putDouble( "windowY", bounds.getY() );
+ mState.putDouble( "windowWidth", bounds.getWidth() );
+ mState.putDouble( "windowHeight", bounds.getHeight() );
}
-
- state.putBoolean( "windowMaximized", stage.isMaximized() );
- state.putBoolean( "windowFullScreen", stage.isFullScreen() );
+
+ mState.putBoolean( "windowMaximized", mStage.isMaximized() );
+ mState.putBoolean( "windowFullScreen", mStage.isFullScreen() );
}
private void restore() {
- final double x = state.getDouble( "windowX", Double.NaN );
- final double y = state.getDouble( "windowY", Double.NaN );
- final double w = state.getDouble( "windowWidth", Double.NaN );
- final double h = state.getDouble( "windowHeight", Double.NaN );
- final boolean maximized = state.getBoolean( "windowMaximized", false );
- final boolean fullScreen = state.getBoolean( "windowFullScreen", false );
+ final double x = mState.getDouble( "windowX", Double.NaN );
+ final double y = mState.getDouble( "windowY", Double.NaN );
+ final double w = mState.getDouble( "windowWidth", Double.NaN );
+ final double h = mState.getDouble( "windowHeight", Double.NaN );
+ final boolean maximized = mState.getBoolean( "windowMaximized", false );
+ final boolean fullScreen = mState.getBoolean( "windowFullScreen", false );
if( !Double.isNaN( x ) && !Double.isNaN( y ) ) {
- stage.setX( x );
- stage.setY( y );
+ mStage.setX( x );
+ mStage.setY( y );
} // else: default behavior is center on screen
if( !Double.isNaN( w ) && !Double.isNaN( h ) ) {
- stage.setWidth( w );
- stage.setHeight( h );
+ mStage.setWidth( w );
+ mStage.setHeight( h );
} // else: default behavior is use scene size
- if( fullScreen != stage.isFullScreen() ) {
- stage.setFullScreen( fullScreen );
+ if( fullScreen != mStage.isFullScreen() ) {
+ mStage.setFullScreen( fullScreen );
}
-
- if( maximized != stage.isMaximized() ) {
- stage.setMaximized( maximized );
+
+ if( maximized != mStage.isMaximized() ) {
+ mStage.setMaximized( maximized );
}
}
return;
}
-
+
runLaterPending = true;
private boolean isNormalState() {
- return !stage.isIconified() && !stage.isMaximized() && !stage.isFullScreen();
+ return !mStage.isIconified() &&
+ !mStage.isMaximized() &&
+ !mStage.isFullScreen();
}
private Rectangle getStageBounds() {
- return new Rectangle( stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight() );
+ return new Rectangle(
+ mStage.getX(),
+ mStage.getY(),
+ mStage.getWidth(),
+ mStage.getHeight()
+ );
}
}
src/main/resources/com/scrivenvar/messages.properties
# ########################################################################
-FileEditor.untitled=Untitled
FileEditor.loadFailed.message=Failed to load ''{0}''.\n\nReason: {1}
FileEditor.loadFailed.title=Load
src/main/resources/com/scrivenvar/settings.properties
# reference can be inserted.
file.default=untitled.md
+file.definition.default=variables.yaml
# ########################################################################