| 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 |
|---|
| <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"> | ||
| */ | ||
| private String getTabTitle() { | ||
| - final Path filePath = getPath(); | ||
| - | ||
| - return (filePath == null) | ||
| - ? Messages.get( "FileEditor.untitled" ) | ||
| - : filePath.getFileName().toString(); | ||
| + return getPath().getFileName().toString(); | ||
| } | ||
| ".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; | ||
| } | ||
| 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 ); | ||
| } | ||
| } |
| 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() ); | ||
| } | ||
| 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; | ||
| + } | ||
| + | ||
| } | ||
| 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 ) { | ||
| + } | ||
| /** | ||
| -/* | ||
| - * 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 ""; | ||
| - } | ||
| -} | ||
| -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 ); | ||
| - } | ||
| -} | ||
| 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; | ||
| /** |
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| 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() { | ||
| } | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| 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 | ||
| + ); | ||
| } | ||
| 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() | ||
| + ); | ||
| } | ||
| } | ||
| # ######################################################################## | ||
| -FileEditor.untitled=Untitled | ||
| FileEditor.loadFailed.message=Failed to load ''{0}''.\n\nReason: {1} | ||
| FileEditor.loadFailed.title=Load |
| # reference can be inserted. | ||
| file.default=untitled.md | ||
| +file.definition.default=variables.yaml | ||
| # ######################################################################## |