Dave Jarvis' Repositories

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

Removed references to user data. Added property for notifying watchers when the active tab is ready. Removed excessive imports (IDE bug). Clear HTML and definitions panes when last tab is closed.

Authordjarvis <email>
Date2016-12-11 16:29:17 GMT-0800
Commit912fad1e9574de6ca8e67339e4fb05e0cd8994ca
Parent6801a7f
CHANGES.md
# Change Log
+## 0.7
+
+- Load YAML variables from files
+
## 0.6
-- Bug fixes synchronized scrolling
+- Fixed synchronized scrolling with preview panel
- Added universal character encoding detection
- Removed options panel
- Added `Ctrl+Space` hot key for quick variable injection
- Replaced commonmark-java with flexmark
-- Added generic CARETPOSITION into document to scroll preview pane
+- Insert `CARETPOSITION` into document for preview pane scroll position reference
## 0.4
CREDITS.md
* Vladimir Schneider: [flexmark](https://website.com)
* Jens Deters: [FontAwesomeFX](https://bitbucket.org/Jerady/fontawesomefx)
- * Apache Tika Team: [Apache Tika](https://tika.apache.org/)
+ * Shy Shalom, Kohei Taketa: [juniversalchardet](https://github.com/takscape/juniversalchardet)
src/main/java/com/scrivenvar/FileEditorTab.java
/**
- * Forwards to the editor pane's listeners for paragraph change events.
+ * Forwards to the editor pane's listeners for caret paragraph change events.
*
* @param listener The listener to notify when the caret changes paragraphs.
*/
public void addCaretParagraphListener( final ChangeListener<Integer> listener){
getEditorPane().addCaretParagraphListener( listener );
}
/**
- * Delegates the request to the editor pane.
+ * Forwards the request to the editor pane.
*
* @return The text to process.
src/main/java/com/scrivenvar/FileEditorTabPane.java
*/
public final class FileEditorTabPane extends TabPane {
-
- private final static String FILTER_PREFIX = "Dialog.file.choose.filter";
-
- private final Options options = Services.load( Options.class );
- private final Settings settings = Services.load( Settings.class );
- private final AlertService alertService = Services.load( AlertService.class );
-
- private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>();
- private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper();
-
- public FileEditorTabPane() {
- final ObservableList<Tab> tabs = getTabs();
-
- setFocusTraversable( false );
- setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
-
- addTabChangeListener( (ObservableValue<? extends Tab> tabPane,
- final Tab oldTab, final Tab newTab) -> {
- if( newTab != null ) {
- activeFileEditor.set( (FileEditorTab)newTab.getUserData() );
- }
- } );
-
- final ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> {
- for( final Tab tab : tabs ) {
- if( ((FileEditorTab)tab.getUserData()).isModified() ) {
- this.anyFileEditorModified.set( true );
- break;
- }
- }
- };
-
- tabs.addListener( (ListChangeListener<Tab>)change -> {
- while( change.next() ) {
- if( change.wasAdded() ) {
- change.getAddedSubList().stream().forEach( (tab) -> {
- ((FileEditorTab)tab.getUserData()).modifiedProperty().addListener( modifiedListener );
- } );
- } else if( change.wasRemoved() ) {
- change.getRemoved().stream().forEach( (tab) -> {
- ((FileEditorTab)tab.getUserData()).modifiedProperty().removeListener( modifiedListener );
- } );
- }
- }
-
- // Changes in the tabs may also change anyFileEditorModified property
- // (e.g. closed modified file)
- modifiedListener.changed( null, null, null );
- } );
- }
-
- public <T extends Event, U extends T> void addEventListener(
- final EventPattern<? super T, ? extends U> event,
- final Consumer<? super U> consumer ) {
- getActiveFileEditor().addEventListener( event, consumer );
- }
-
- /**
- * Delegates to the active file editor pane, and, ultimately, to its text
- * area.
- *
- * @param map The map of methods to events.
- */
- public void addEventListener( final InputMap<InputEvent> map ) {
- getActiveFileEditor().addEventListener( map );
- }
-
- /**
- * Remove a keyboard event listener from the active file editor.
- *
- * @param map The keyboard events to remove.
- */
- public void removeEventListener( final InputMap<InputEvent> map ) {
- getActiveFileEditor().removeEventListener( map );
- }
-
- /**
- * Allows observers to be notified when the current file editor tab changes.
- *
- * @param listener The listener to notify of tab change events.
- */
- public void addTabChangeListener( 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();
- }
-
- public FileEditorTab getActiveFileEditor() {
- return this.activeFileEditor.get();
- }
-
- ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
- return this.activeFileEditor.getReadOnlyProperty();
- }
-
- ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
- return this.anyFileEditorModified.getReadOnlyProperty();
- }
-
- private FileEditorTab createFileEditor( final Path path ) {
- final FileEditorTab tab = new FileEditorTab( path );
-
- tab.setOnCloseRequest( e -> {
- if( !canCloseEditor( tab ) ) {
- e.consume();
- }
- } );
-
- return tab;
- }
-
- Node getNode() {
- return this;
- }
-
- /**
- * Called when the user selects New from the File menu.
- *
- * @return The newly added tab.
- */
- FileEditorTab newEditor() {
- final FileEditorTab tab = createFileEditor( null );
-
- getTabs().add( tab );
- getSelectionModel().select( tab );
- return tab;
- }
-
- List<FileEditorTab> openFileDialog() {
- final FileChooser dialog
- = createFileChooser( get( "Dialog.file.choose.open.title" ) );
- final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
-
- return (files != null && !files.isEmpty())
- ? openFiles( files )
- : new ArrayList<>();
- }
-
- /**
- * 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.
- *
- * @return A list of files that can be opened in text editors.
- */
- private List<FileEditorTab> openFiles( final List<File> files ) {
- final List<FileEditorTab> openedEditors = new ArrayList<>();
-
- final FileTypePredicate predicate
- = new FileTypePredicate( createExtensionFilter( "definition" ).getExtensions() );
-
- // The user might have opened muliple 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 );
- editors.removeAll( definitions );
-
- // If there are any editor-friendly files opened (e.g,. Markdown, XML), then
- // open them up in new tabs.
- if( editors.size() > 0 ) {
- saveLastDirectory( editors.get( 0 ) );
- openedEditors.addAll( openEditors( editors, 0 ) );
- }
-
- if( definitions.size() > 0 ) {
- openDefinition( definitions.get( 0 ) );
- }
-
- return openedEditors;
- }
-
- private List<FileEditorTab> openEditors( final List<File> files, final int activeIndex ) {
- final int fileTally = files.size();
- final List<FileEditorTab> editors = new ArrayList<>( fileTally );
- final List<Tab> tabs = getTabs();
-
- // Close single unmodified "Untitled" tab.
- if( tabs.size() == 1 ) {
- final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 ).getUserData());
-
- if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
- closeEditor( fileEditor, false );
- }
- }
-
- for( int i = 0; i < fileTally; i++ ) {
- final Path path = files.get( i ).toPath();
-
- // Check whether file is already opened.
- FileEditorTab fileEditor = findEditor( path );
-
- if( fileEditor == null ) {
- fileEditor = createFileEditor( path );
- getTabs().add( fileEditor );
- editors.add( fileEditor );
- }
-
- // Select first file.
- if( i == activeIndex ) {
- getSelectionModel().select( fileEditor );
- }
- }
-
- return editors;
- }
-
- /**
- * 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 ) {
- System.out.println( "open definition file: " + definition.toString() );
- }
-
- boolean saveEditor( final FileEditorTab fileEditor ) {
- if( fileEditor == null || !fileEditor.isModified() ) {
- return true;
- }
-
- if( fileEditor.getPath() == null ) {
- getSelectionModel().select( fileEditor );
-
- final FileChooser fileChooser = createFileChooser( Messages.get( "Dialog.file.choose.save.title" ) );
- final File file = fileChooser.showSaveDialog( getWindow() );
- if( file == null ) {
- return false;
- }
-
- saveLastDirectory( file );
- fileEditor.setPath( file.toPath() );
- }
-
- return fileEditor.save();
- }
-
- boolean saveAllEditors() {
- boolean success = true;
-
- for( FileEditorTab fileEditor : getAllEditors() ) {
- if( !saveEditor( fileEditor ) ) {
- success = false;
- }
- }
-
- return success;
- }
-
- boolean canCloseEditor( final FileEditorTab tab ) {
- if( !tab.isModified() ) {
- return true;
- }
-
- final AlertMessage message = getAlertService().createAlertMessage(
- Messages.get( "Alert.file.close.title" ),
- Messages.get( "Alert.file.close.text" ),
- tab.getText()
- );
-
- final Alert alert = getAlertService().createAlertConfirmation( message );
- final ButtonType response = alert.showAndWait().get();
-
- return response == YES ? saveEditor( tab ) : response == NO;
- }
-
- private AlertService getAlertService() {
- return this.alertService;
- }
-
- boolean closeEditor( FileEditorTab fileEditor, boolean save ) {
- if( fileEditor == null ) {
- return true;
- }
-
- final Tab tab = fileEditor;
-
- 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 ).getUserData();
- }
-
- 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 = getState().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( "markdown" ) );
- list.add( createExtensionFilter( "definition" ) );
- list.add( createExtensionFilter( "xml" ) );
- list.add( createExtensionFilter( "all" ) );
- return list;
- }
-
- private ExtensionFilter createExtensionFilter( final String filetype ) {
- final String tKey = String.format( "%s.title.%s", FILTER_PREFIX, filetype );
- final String eKey = String.format( "%s.ext.%s", FILTER_PREFIX, filetype );
-
- return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
- }
-
- private List<String> getExtensions( final String key ) {
- return getStringSettingList( key );
- }
-
- private List<String> getStringSettingList( String key ) {
- return getStringSettingList( key, null );
- }
-
- private List<String> getStringSettingList( String key, List<String> values ) {
- return getSettings().getStringSettingList( key, values );
- }
-
- private void saveLastDirectory( final File file ) {
- getState().put( "lastDirectory", file.getParent() );
- }
-
- public void restorePreferences() {
- int activeIndex = 0;
-
- final Preferences preferences = getState();
- 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();
- return;
- }
-
- 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;
-
- if( fileEditor.getPath() != null ) {
- fileNames.add( fileEditor.getPath().toString() );
- }
- }
-
- final Preferences preferences = getState();
- Utils.putPrefsStrings( preferences, "file", fileNames.toArray( new String[ fileNames.size() ] ) );
-
- final FileEditorTab activeEditor = getActiveFileEditor();
-
- if( activeEditor != null && activeEditor.getPath() != null ) {
- preferences.put( "activeFile", activeEditor.getPath().toString() );
- } else {
- preferences.remove( "activeFile" );
- }
- }
-
- private Settings getSettings() {
- return this.settings;
- }
-
- protected Options getOptions() {
- return this.options;
- }
-
- private Window getWindow() {
- return getScene().getWindow();
- }
-
+
+ private final static String FILTER_PREFIX = "Dialog.file.choose.filter";
+
+ private final Options options = Services.load( Options.class );
+ private final Settings settings = Services.load( Settings.class );
+ private final AlertService alertService = Services.load( AlertService.class );
+
+ private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>();
+ private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper();
+ private final ReadOnlyObjectWrapper<FileEditorTab> onOpen = new ReadOnlyObjectWrapper<>();
+
+ public FileEditorTabPane() {
+ final ObservableList<Tab> tabs = getTabs();
+
+ setFocusTraversable( false );
+ setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
+
+ addTabChangeListener(
+ (ObservableValue<? extends Tab> tabPane,
+ final Tab oldTab, final Tab newTab) -> {
+
+ if( newTab != null ) {
+ activeFileEditor.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().stream().forEach( (tab) -> {
+ ((FileEditorTab)tab).modifiedProperty().addListener( modifiedListener );
+ } );
+ } else if( change.wasRemoved() ) {
+ change.getRemoved().stream().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 );
+ }
+ );
+ }
+
+ public <T extends Event, U extends T> void addEventListener(
+ final EventPattern<? super T, ? extends U> event,
+ final Consumer<? super U> consumer ) {
+ getActiveFileEditor().addEventListener( event, consumer );
+ }
+
+ /**
+ * Delegates to the active file editor pane, and, ultimately, to its text
+ * area.
+ *
+ * @param map The map of methods to events.
+ */
+ public void addEventListener( final InputMap<InputEvent> map ) {
+ getActiveFileEditor().addEventListener( map );
+ }
+
+ /**
+ * Remove a keyboard event listener from the active file editor.
+ *
+ * @param map The keyboard events to remove.
+ */
+ public void removeEventListener( final InputMap<InputEvent> map ) {
+ getActiveFileEditor().removeEventListener( map );
+ }
+
+ /**
+ * Allows observers to be notified when the current file editor tab changes.
+ *
+ * @param listener The listener to notify of tab change events.
+ */
+ public void addTabChangeListener( 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();
+ }
+
+ public FileEditorTab getActiveFileEditor() {
+ return this.activeFileEditor.get();
+ }
+
+ public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
+ return this.activeFileEditor.getReadOnlyProperty();
+ }
+
+ ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
+ return this.anyFileEditorModified.getReadOnlyProperty();
+ }
+
+ private FileEditorTab createFileEditor( final Path path ) {
+ final FileEditorTab tab = new FileEditorTab( path );
+
+ tab.setOnCloseRequest( e -> {
+ if( !canCloseEditor( tab ) ) {
+ e.consume();
+ }
+ } );
+
+ return tab;
+ }
+
+ Node getNode() {
+ return this;
+ }
+
+ /**
+ * Called when the user selects New from the File menu.
+ *
+ * @return The newly added tab.
+ */
+ FileEditorTab newEditor() {
+ final FileEditorTab tab = createFileEditor( null );
+
+ getTabs().add( tab );
+ getSelectionModel().select( tab );
+ return tab;
+ }
+
+ List<FileEditorTab> openFileDialog() {
+ final FileChooser dialog
+ = createFileChooser( get( "Dialog.file.choose.open.title" ) );
+ final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
+
+ return (files != null && !files.isEmpty())
+ ? openFiles( files )
+ : new ArrayList<>();
+ }
+
+ /**
+ * 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.
+ *
+ * @return A list of files that can be opened in text editors.
+ */
+ private List<FileEditorTab> openFiles( final List<File> files ) {
+ final List<FileEditorTab> openedEditors = new ArrayList<>();
+
+ final FileTypePredicate predicate
+ = new FileTypePredicate( createExtensionFilter( "definition" ).getExtensions() );
+
+ // The user might have opened muliple 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 );
+ editors.removeAll( definitions );
+
+ // If there are any editor-friendly files opened (e.g,. Markdown, XML), then
+ // open them up in new tabs.
+ if( editors.size() > 0 ) {
+ saveLastDirectory( editors.get( 0 ) );
+ openedEditors.addAll( openEditors( editors, 0 ) );
+ }
+
+ if( definitions.size() > 0 ) {
+ openDefinition( definitions.get( 0 ) );
+ }
+
+ return openedEditors;
+ }
+
+ private List<FileEditorTab> openEditors( final List<File> files, final int activeIndex ) {
+ final int fileTally = files.size();
+ final List<FileEditorTab> editors = new ArrayList<>( fileTally );
+ 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();
+
+ // Check whether file is already opened.
+ FileEditorTab fileEditor = findEditor( path );
+
+ if( fileEditor == null ) {
+ fileEditor = createFileEditor( path );
+ getTabs().add( fileEditor );
+ editors.add( fileEditor );
+ }
+
+ // Select first file.
+ if( i == activeIndex ) {
+ getSelectionModel().select( fileEditor );
+ }
+ }
+
+ if( activeIndex < fileTally ) {
+ getOnOpen().set( editors.get( activeIndex ) );
+ }
+
+ return editors;
+ }
+
+ private ReadOnlyObjectWrapper<FileEditorTab> getOnOpen() {
+ return this.onOpen;
+ }
+
+ public ReadOnlyObjectProperty<FileEditorTab> onOpenProperty() {
+ return getOnOpen().getReadOnlyProperty();
+ }
+
+ /**
+ * 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 ) {
+ System.out.println( "open definition file: " + definition.toString() );
+ }
+
+ boolean saveEditor( final FileEditorTab fileEditor ) {
+ if( fileEditor == null || !fileEditor.isModified() ) {
+ return true;
+ }
+
+ if( fileEditor.getPath() == null ) {
+ getSelectionModel().select( fileEditor );
+
+ final FileChooser fileChooser = createFileChooser( Messages.get( "Dialog.file.choose.save.title" ) );
+ final File file = fileChooser.showSaveDialog( getWindow() );
+ if( file == null ) {
+ return false;
+ }
+
+ saveLastDirectory( file );
+ fileEditor.setPath( file.toPath() );
+ }
+
+ return fileEditor.save();
+ }
+
+ boolean saveAllEditors() {
+ boolean success = true;
+
+ for( FileEditorTab fileEditor : getAllEditors() ) {
+ if( !saveEditor( fileEditor ) ) {
+ success = false;
+ }
+ }
+
+ return success;
+ }
+
+ boolean canCloseEditor( final FileEditorTab tab ) {
+ if( !tab.isModified() ) {
+ return true;
+ }
+
+ final AlertMessage message = getAlertService().createAlertMessage(
+ Messages.get( "Alert.file.close.title" ),
+ Messages.get( "Alert.file.close.text" ),
+ tab.getText()
+ );
+
+ final Alert alert = getAlertService().createAlertConfirmation( message );
+ final ButtonType response = alert.showAndWait().get();
+
+ return response == YES ? saveEditor( tab ) : response == NO;
+ }
+
+ private AlertService getAlertService() {
+ return this.alertService;
+ }
+
+ boolean closeEditor( FileEditorTab fileEditor, boolean save ) {
+ if( fileEditor == null ) {
+ return true;
+ }
+
+ final Tab tab = fileEditor;
+
+ 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 = getState().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( "markdown" ) );
+ list.add( createExtensionFilter( "definition" ) );
+ list.add( createExtensionFilter( "xml" ) );
+ list.add( createExtensionFilter( "all" ) );
+ return list;
+ }
+
+ private ExtensionFilter createExtensionFilter( final String filetype ) {
+ final String tKey = String.format( "%s.title.%s", FILTER_PREFIX, filetype );
+ final String eKey = String.format( "%s.ext.%s", FILTER_PREFIX, filetype );
+
+ return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
+ }
+
+ private List<String> getExtensions( final String key ) {
+ return getStringSettingList( key );
+ }
+
+ private List<String> getStringSettingList( String key ) {
+ return getStringSettingList( key, null );
+ }
+
+ private List<String> getStringSettingList( String key, List<String> values ) {
+ return getSettings().getStringSettingList( key, values );
+ }
+
+ private void saveLastDirectory( final File file ) {
+ getState().put( "lastDirectory", file.getParent() );
+ }
+
+ public void restorePreferences() {
+ int activeIndex = 0;
+
+ final Preferences preferences = getState();
+ 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 = getState();
+ Utils.putPrefsStrings( preferences, "file", fileNames.toArray( new String[ fileNames.size() ] ) );
+
+ 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 Window getWindow() {
+ return getScene().getWindow();
+ }
+
protected Preferences getState() {
return getOptions().getState();
src/main/java/com/scrivenvar/MainWindow.java
import static com.scrivenvar.Constants.LOGO_32;
-import static com.scrivenvar.Messages.get;
-import com.scrivenvar.definition.DefinitionPane;
-import com.scrivenvar.editor.MarkdownEditorPane;
-import com.scrivenvar.editor.VariableNameInjector;
-import com.scrivenvar.preview.HTMLPreviewPane;
-import com.scrivenvar.processors.HTMLPreviewProcessor;
-import com.scrivenvar.processors.MarkdownCaretInsertionProcessor;
-import com.scrivenvar.processors.MarkdownCaretReplacementProcessor;
-import com.scrivenvar.processors.MarkdownProcessor;
-import com.scrivenvar.processors.Processor;
-import com.scrivenvar.processors.VariableProcessor;
-import com.scrivenvar.service.Options;
-import com.scrivenvar.util.Action;
-import com.scrivenvar.util.ActionUtils;
-import static com.scrivenvar.util.StageState.K_PANE_SPLIT_DEFINITION;
-import static com.scrivenvar.util.StageState.K_PANE_SPLIT_EDITOR;
-import static com.scrivenvar.util.StageState.K_PANE_SPLIT_PREVIEW;
-import com.scrivenvar.yaml.YamlParser;
-import com.scrivenvar.yaml.YamlTreeAdapter;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.prefs.Preferences;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.value.ObservableBooleanValue;
-import javafx.beans.value.ObservableValue;
-import javafx.collections.ListChangeListener.Change;
-import javafx.collections.ObservableList;
-import javafx.event.Event;
-import javafx.scene.Node;
-import javafx.scene.Scene;
-import javafx.scene.control.Alert;
-import javafx.scene.control.Alert.AlertType;
-import javafx.scene.control.Menu;
-import javafx.scene.control.MenuBar;
-import javafx.scene.control.SplitPane;
-import javafx.scene.control.Tab;
-import javafx.scene.control.ToolBar;
-import javafx.scene.control.TreeView;
-import javafx.scene.image.Image;
-import javafx.scene.image.ImageView;
-import static javafx.scene.input.KeyCode.ESCAPE;
-import javafx.scene.input.KeyEvent;
-import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
-import static javafx.scene.input.KeyEvent.KEY_PRESSED;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.layout.VBox;
-import javafx.stage.Window;
-import javafx.stage.WindowEvent;
-
-/**
- * Main window containing a tab pane in the center for file editors.
- *
- * @author Karl Tauber and White Magic Software, Ltd.
- */
-public class MainWindow {
-
- private final Options options = Services.load( Options.class );
-
- private Scene scene;
-
- private TreeView<String> treeView;
- private DefinitionPane definitionPane;
- private FileEditorTabPane fileEditorPane;
- private HTMLPreviewPane previewPane;
-
- private VariableNameInjector variableNameInjector;
-
- private YamlTreeAdapter yamlTreeAdapter;
- private YamlParser yamlParser;
-
- private MenuBar menuBar;
-
- public MainWindow() {
- initLayout();
- initTabAddedListener();
- restorePreferences();
- initTabChangeListener();
- initVariableNameInjector();
- }
-
- private void initLayout() {
- final SplitPane splitPane = new SplitPane(
- getDefinitionPane().getNode(),
- getFileEditorPane().getNode(),
- getPreviewPane().getNode() );
-
- splitPane.setDividerPositions(
- getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
- getFloat( K_PANE_SPLIT_EDITOR, .45f ),
- getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
-
- // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
- final BorderPane borderPane = new BorderPane();
- borderPane.setPrefSize( 1024, 800 );
- borderPane.setTop( createMenuBar() );
- borderPane.setCenter( splitPane );
-
- final Scene appScene = new Scene( borderPane );
- setScene( appScene );
- appScene.getStylesheets().add( Constants.STYLESHEET_PREVIEW );
- appScene.windowProperty().addListener(
- (observable, oldWindow, newWindow) -> {
- newWindow.setOnCloseRequest( e -> {
- if( !getFileEditorPane().closeAllEditors() ) {
- e.consume();
- }
- } );
-
- // Workaround JavaFX bug: deselect menubar if window loses focus.
- newWindow.focusedProperty().addListener(
- (obs, oldFocused, newFocused) -> {
- if( !newFocused ) {
- // Send an ESC key event to the menubar
- this.menuBar.fireEvent(
- new KeyEvent(
- KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE,
- false, false, false, false ) );
- }
- } );
- } );
- }
-
- private void initVariableNameInjector() {
- setVariableNameInjector( new VariableNameInjector(
- getFileEditorPane(),
- getDefinitionPane() )
- );
- }
-
- private Window getWindow() {
- return getScene().getWindow();
- }
-
- public Scene getScene() {
- return this.scene;
- }
-
- private void setScene( Scene scene ) {
- this.scene = scene;
- }
-
- /**
- * Creates a boolean property that is bound to another boolean value of the
- * active editor.
- */
- private BooleanProperty createActiveBooleanProperty(
- final Function<FileEditorTab, ObservableBooleanValue> func ) {
-
- final BooleanProperty b = new SimpleBooleanProperty();
- final FileEditorTab tab = getActiveFileEditor();
-
- if( tab != null ) {
- b.bind( func.apply( tab ) );
- }
-
- getFileEditorPane().activeFileEditorProperty().addListener(
- (observable, oldFileEditor, newFileEditor) -> {
- b.unbind();
-
- if( newFileEditor != null ) {
- b.bind( func.apply( newFileEditor ) );
- } else {
- b.set( false );
- }
- } );
-
- return b;
- }
-
- //---- File actions -------------------------------------------------------
- private void fileNew() {
- getFileEditorPane().newEditor();
- }
-
- private void fileOpen() {
- getFileEditorPane().openFileDialog();
- }
-
- private void fileClose() {
- getFileEditorPane().closeEditor( getActiveFileEditor(), true );
- }
-
- private void fileCloseAll() {
- getFileEditorPane().closeAllEditors();
- }
-
- private void fileSave() {
- getFileEditorPane().saveEditor( getActiveFileEditor() );
- }
-
- private void fileSaveAll() {
- getFileEditorPane().saveAllEditors();
- }
-
- private void fileExit() {
- final Window window = getWindow();
- Event.fireEvent( window,
- new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) );
- }
-
- //---- Help actions -------------------------------------------------------
- private void helpAbout() {
- Alert alert = new Alert( AlertType.INFORMATION );
- alert.setTitle( Messages.get( "Dialog.about.title" ) );
- alert.setHeaderText( Messages.get( "Dialog.about.header" ) );
- alert.setContentText( Messages.get( "Dialog.about.content" ) );
- alert.setGraphic( new ImageView( new Image( LOGO_32 ) ) );
- alert.initOwner( getWindow() );
-
- alert.showAndWait();
- }
-
- private FileEditorTabPane getFileEditorPane() {
- if( this.fileEditorPane == null ) {
- this.fileEditorPane = createFileEditorPane();
- }
-
- return this.fileEditorPane;
- }
-
- /**
- * Create an editor pane to hold file editor tabs.
- *
- * @return A new instance, never null.
- */
- private FileEditorTabPane createFileEditorPane() {
- return new FileEditorTabPane();
- }
-
- /**
- * Reloads the preferences from the previous load.
- */
- private void restorePreferences() {
- getFileEditorPane().restorePreferences();
- }
-
- private void initTabAddedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Make sure the text processor kicks off when new files are opened.
- final ObservableList<Tab> tabs = editorPane.getTabs();
-
- // Update the preview pane on tab changes.
- tabs.addListener( (final Change<? extends Tab> change) -> {
- while( change.next() ) {
- if( change.wasAdded() ) {
- // Multiple tabs can be added simultaneously.
- for( final Tab newTab : change.getAddedSubList() ) {
- final FileEditorTab tab = (FileEditorTab)newTab;
-
- initTextChangeListener( tab );
- initCaretParagraphListener( tab );
- process( tab );
- }
- }
- }
- } );
- }
-
- /**
- * Listen for tab changes.
- */
- private void initTabChangeListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Update the preview pane changing tabs.
- editorPane.addTabChangeListener(
- (ObservableValue<? extends Tab> tabPane,
- final Tab oldTab, final Tab newTab) -> {
-
- final FileEditorTab tab = (FileEditorTab)newTab;
-
- if( tab != null ) {
- // When a new tab is selected, ensure that the base path to images
- // is set correctly.
- getPreviewPane().setPath( tab.getPath() );
- process( tab );
- }
- } );
- }
-
- private void initTextChangeListener( final FileEditorTab tab ) {
- tab.addTextChangeListener( (ObservableValue<? extends String> editor,
- final String oldValue, final String newValue) -> {
- process( tab );
- } );
- }
-
- private void initCaretParagraphListener( final FileEditorTab tab ) {
- tab.addCaretParagraphListener( (ObservableValue<? extends Integer> editor,
- final Integer oldValue, final Integer newValue) -> {
- process( tab );
- } );
- }
-
- /**
- * Called whenever the preview pane becomes out of sync with the file editor
- * tab. This can be called when the text changes, the caret paragraph changes,
- * or the file tab changes.
- *
- * @param tab The file editor tab that has been changed in some fashion.
- */
- private void process( final FileEditorTab tab ) {
- // TODO: Use a factory based on the filename extension. The default
- // extension will be for a markdown file (e.g., on file new).
- final Processor<String> hpp = new HTMLPreviewProcessor( getPreviewPane() );
- final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp );
- final Processor<String> mp = new MarkdownProcessor( mcrp );
- final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, tab.getCaretPosition() );
- final Processor<String> vp = new VariableProcessor( mcip, getResolvedMap() );
-
- vp.processChain( tab.getEditorText() );
- }
-
- private MarkdownEditorPane getActiveEditor() {
- return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane());
- }
-
- private FileEditorTab getActiveFileEditor() {
- return getFileEditorPane().getActiveFileEditor();
- }
-
- protected DefinitionPane createDefinitionPane() {
- return new DefinitionPane( getTreeView() );
- }
-
- private DefinitionPane getDefinitionPane() {
- if( this.definitionPane == null ) {
- this.definitionPane = createDefinitionPane();
- }
-
- return this.definitionPane;
- }
-
- public MenuBar getMenuBar() {
- return this.menuBar;
- }
-
- public void setMenuBar( MenuBar menuBar ) {
- this.menuBar = menuBar;
- }
-
- public VariableNameInjector getVariableNameInjector() {
- return this.variableNameInjector;
- }
-
- public void setVariableNameInjector( VariableNameInjector variableNameInjector ) {
- this.variableNameInjector = variableNameInjector;
- }
-
- private float getFloat( final String key, final float defaultValue ) {
- return getPreferences().getFloat( key, defaultValue );
- }
-
- private Preferences getPreferences() {
- return getOptions().getState();
- }
-
- private Options getOptions() {
- return this.options;
- }
-
- private synchronized TreeView<String> getTreeView() throws RuntimeException {
- if( this.treeView == null ) {
- try {
- this.treeView = createTreeView();
- } catch( IOException ex ) {
-
- // TODO: Pop an error message.
- throw new RuntimeException( ex );
- }
- }
-
- return this.treeView;
- }
-
- private InputStream asStream( final String resource ) {
- return getClass().getResourceAsStream( resource );
- }
-
- private TreeView<String> createTreeView() throws IOException {
- // TODO: Associate variable file with path to current file.
- return getYamlTreeAdapter().adapt(
- asStream( "/com/scrivenvar/variables.yaml" ),
- get( "Pane.defintion.node.root.title" )
- );
- }
-
- private Map<String, String> getResolvedMap() {
- return getYamlParser().createResolvedMap();
- }
-
- private YamlTreeAdapter getYamlTreeAdapter() {
- if( this.yamlTreeAdapter == null ) {
- setYamlTreeAdapter( new YamlTreeAdapter( getYamlParser() ) );
- }
-
- return this.yamlTreeAdapter;
- }
-
- private void setYamlTreeAdapter( final YamlTreeAdapter yamlTreeAdapter ) {
- this.yamlTreeAdapter = yamlTreeAdapter;
- }
-
- private YamlParser getYamlParser() {
- if( this.yamlParser == null ) {
- setYamlParser( new YamlParser() );
- }
-
- return this.yamlParser;
- }
-
- private void setYamlParser( final YamlParser yamlParser ) {
- this.yamlParser = yamlParser;
- }
-
- private Node createMenuBar() {
- final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
-
- // File actions
- Action fileNewAction = new Action( Messages.get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
- Action fileOpenAction = new Action( Messages.get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
- Action fileCloseAction = new Action( Messages.get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
- Action fileCloseAllAction = new Action( Messages.get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
- Action fileSaveAction = new Action( Messages.get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
- createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
- Action fileSaveAllAction = new Action( Messages.get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
- Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
- Action fileExitAction = new Action( Messages.get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
-
- // Edit actions
- Action editUndoAction = new Action( Messages.get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
- e -> getActiveEditor().undo(),
- createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
- Action editRedoAction = new Action( Messages.get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
- e -> getActiveEditor().redo(),
- createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
-
- // Insert actions
- Action insertBoldAction = new Action( Messages.get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
- e -> getActiveEditor().surroundSelection( "**", "**" ),
- activeFileEditorIsNull );
- Action insertItalicAction = new Action( Messages.get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
- e -> getActiveEditor().surroundSelection( "*", "*" ),
- activeFileEditorIsNull );
- Action insertStrikethroughAction = new Action( Messages.get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
- e -> getActiveEditor().surroundSelection( "~~", "~~" ),
- activeFileEditorIsNull );
- Action insertBlockquoteAction = new Action( Messages.get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
- e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
- activeFileEditorIsNull );
- Action insertCodeAction = new Action( Messages.get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
- e -> getActiveEditor().surroundSelection( "`", "`" ),
- activeFileEditorIsNull );
- Action insertFencedCodeBlockAction = new Action( Messages.get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
- e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", Messages.get( "Main.menu.insert.fenced_code_block.prompt" ) ),
- activeFileEditorIsNull );
-
- Action insertLinkAction = new Action( Messages.get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
- e -> getActiveEditor().insertLink(),
- activeFileEditorIsNull );
- Action insertImageAction = new Action( Messages.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 = Messages.get( "Main.menu.insert.header_" + i );
- final String accelerator = "Shortcut+" + i;
- final String prompt = Messages.get( "Main.menu.insert.header_" + i + ".prompt" );
-
- headers[ i - 1 ] = new Action( text, accelerator, HEADER,
- e -> getActiveEditor().surroundSelection( markup, "", prompt ),
- activeFileEditorIsNull );
- }
-
- Action insertUnorderedListAction = new Action( Messages.get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
- e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
- activeFileEditorIsNull );
- Action insertOrderedListAction = new Action( Messages.get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
- e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
- activeFileEditorIsNull );
- Action insertHorizontalRuleAction = new Action( Messages.get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
- e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
- activeFileEditorIsNull );
-
- // Help actions
- Action helpAboutAction = new Action( Messages.get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
-
- //---- MenuBar ----
- Menu fileMenu = ActionUtils.createMenu( Messages.get( "Main.menu.file" ),
- fileNewAction,
- fileOpenAction,
- null,
- fileCloseAction,
- fileCloseAllAction,
- null,
- fileSaveAction,
- fileSaveAllAction,
- null,
- fileExitAction );
-
- Menu editMenu = ActionUtils.createMenu( Messages.get( "Main.menu.edit" ),
- editUndoAction,
- editRedoAction );
-
- Menu insertMenu = ActionUtils.createMenu( Messages.get( "Main.menu.insert" ),
- insertBoldAction,
- insertItalicAction,
- insertStrikethroughAction,
- insertBlockquoteAction,
- insertCodeAction,
- insertFencedCodeBlockAction,
- null,
- insertLinkAction,
- insertImageAction,
- null,
- headers[ 0 ],
- headers[ 1 ],
- headers[ 2 ],
- headers[ 3 ],
- headers[ 4 ],
- headers[ 5 ],
- null,
- insertUnorderedListAction,
- insertOrderedListAction,
- insertHorizontalRuleAction );
-
- Menu helpMenu = ActionUtils.createMenu( Messages.get( "Main.menu.help" ),
- helpAboutAction );
-
- menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu );
-
- //---- ToolBar ----
- ToolBar toolBar = ActionUtils.createToolBar(
- fileNewAction,
- fileOpenAction,
- fileSaveAction,
- null,
- editUndoAction,
- editRedoAction,
- null,
- insertBoldAction,
- insertItalicAction,
- insertBlockquoteAction,
- insertCodeAction,
- insertFencedCodeBlockAction,
- null,
- insertLinkAction,
- insertImageAction,
- null,
- headers[ 0 ],
- null,
- insertUnorderedListAction,
- insertOrderedListAction );
-
- return new VBox( menuBar, toolBar );
- }
-
- private synchronized HTMLPreviewPane getPreviewPane() {
- if( this.previewPane == null ) {
- this.previewPane = new HTMLPreviewPane();
- }
-
- return this.previewPane;
- }
-
+import static com.scrivenvar.Constants.STYLESHEET_PREVIEW;
+import static com.scrivenvar.Messages.get;
+import com.scrivenvar.definition.DefinitionPane;
+import com.scrivenvar.editor.MarkdownEditorPane;
+import com.scrivenvar.editor.VariableNameInjector;
+import com.scrivenvar.preview.HTMLPreviewPane;
+import com.scrivenvar.processors.HTMLPreviewProcessor;
+import com.scrivenvar.processors.MarkdownCaretInsertionProcessor;
+import com.scrivenvar.processors.MarkdownCaretReplacementProcessor;
+import com.scrivenvar.processors.MarkdownProcessor;
+import com.scrivenvar.processors.Processor;
+import com.scrivenvar.processors.VariableProcessor;
+import com.scrivenvar.service.Options;
+import com.scrivenvar.util.Action;
+import com.scrivenvar.util.ActionUtils;
+import static com.scrivenvar.util.StageState.K_PANE_SPLIT_DEFINITION;
+import static com.scrivenvar.util.StageState.K_PANE_SPLIT_EDITOR;
+import static com.scrivenvar.util.StageState.K_PANE_SPLIT_PREVIEW;
+import com.scrivenvar.yaml.YamlParser;
+import com.scrivenvar.yaml.YamlTreeAdapter;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.prefs.Preferences;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ObservableBooleanValue;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ListChangeListener.Change;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuBar;
+import javafx.scene.control.SplitPane;
+import javafx.scene.control.Tab;
+import javafx.scene.control.ToolBar;
+import javafx.scene.control.TreeView;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import static javafx.scene.input.KeyCode.ESCAPE;
+import javafx.scene.input.KeyEvent;
+import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
+import static javafx.scene.input.KeyEvent.KEY_PRESSED;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import javafx.stage.Window;
+import javafx.stage.WindowEvent;
+
+/**
+ * Main window containing a tab pane in the center for file editors.
+ *
+ * @author Karl Tauber and White Magic Software, Ltd.
+ */
+public class MainWindow {
+
+ private final Options options = Services.load( Options.class );
+
+ private Scene scene;
+
+ private TreeView<String> treeView;
+ private DefinitionPane definitionPane;
+ private FileEditorTabPane fileEditorPane;
+ private HTMLPreviewPane previewPane;
+
+ private VariableNameInjector variableNameInjector;
+
+ private YamlTreeAdapter yamlTreeAdapter;
+ private YamlParser yamlParser;
+
+ private MenuBar menuBar;
+
+ public MainWindow() {
+ initLayout();
+ initOnOpenListener();
+ initTabAddedListener();
+ restorePreferences();
+ initTabChangeListener();
+ initVariableNameInjector();
+ }
+
+ private void initLayout() {
+ final SplitPane splitPane = new SplitPane(
+ getDefinitionPane().getNode(),
+ getFileEditorPane().getNode(),
+ getPreviewPane().getNode() );
+
+ splitPane.setDividerPositions(
+ getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
+ getFloat( K_PANE_SPLIT_EDITOR, .45f ),
+ getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
+
+ // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
+ final BorderPane borderPane = new BorderPane();
+ borderPane.setPrefSize( 1024, 800 );
+ borderPane.setTop( createMenuBar() );
+ borderPane.setCenter( splitPane );
+
+ final Scene appScene = new Scene( borderPane );
+ setScene( appScene );
+ appScene.getStylesheets().add( STYLESHEET_PREVIEW );
+ appScene.windowProperty().addListener(
+ (observable, oldWindow, newWindow) -> {
+ newWindow.setOnCloseRequest( e -> {
+ if( !getFileEditorPane().closeAllEditors() ) {
+ e.consume();
+ }
+ } );
+
+ // Workaround JavaFX bug: deselect menubar if window loses focus.
+ newWindow.focusedProperty().addListener(
+ (obs, oldFocused, newFocused) -> {
+ if( !newFocused ) {
+ // Send an ESC key event to the menubar
+ this.menuBar.fireEvent(
+ new KeyEvent(
+ KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE,
+ false, false, false, false ) );
+ }
+ }
+ );
+ }
+ );
+ }
+
+ /**
+ * 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 );
+ }
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * Listen for the active editor to change once one or more files have been
+ * opened.
+ */
+ private void initOnOpenListener() {
+ getFileEditorPane().onOpenProperty().addListener(
+ (ObservableValue<? extends FileEditorTab> tabPane,
+ final FileEditorTab oldTab, final FileEditorTab tab) -> {
+ initialize( tab );
+ }
+ );
+ }
+
+ /**
+ * Listen for new tab selection events.
+ */
+ private void initTabChangeListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Update the preview pane changing tabs.
+ editorPane.addTabChangeListener(
+ (ObservableValue<? extends Tab> tabPane,
+ final Tab oldTab, final Tab newTab) -> {
+
+ final FileEditorTab tab = (FileEditorTab)newTab;
+
+ if( tab != null ) {
+ initialize( tab );
+
+ // Synchronize the preview with the edited text.
+ updatePreviewPane( tab );
+ } else {
+ closeRemainingTab();
+ }
+ }
+ );
+ }
+
+ private void closeRemainingTab() {
+ getPreviewPane().clear();
+ getDefinitionPane().clear();
+ }
+
+ /**
+ * Initializes the given tab by ensure the preview pane's path is set and the
+ * definition pane for that tab is loaded.
+ *
+ * TODO: Ensure this is only called once.
+ *
+ * @param tab The tab to initialize.
+ */
+ private void initialize( final FileEditorTab tab ) {
+ // Ensure that the base path to images is set correctly.
+ getPreviewPane().setPath( tab.getPath() );
+
+ // Ensure that the definitions is associated with the edited file.
+ updateDefinitionPane( tab );
+ }
+
+ private void initTextChangeListener( final FileEditorTab tab ) {
+ tab.addTextChangeListener(
+ (ObservableValue<? extends String> editor,
+ final String oldValue, final String newValue) -> {
+ updatePreviewPane( tab );
+ }
+ );
+ }
+
+ private void initCaretParagraphListener( final FileEditorTab tab ) {
+ tab.addCaretParagraphListener(
+ (ObservableValue<? extends Integer> editor,
+ final Integer oldValue, final Integer newValue) -> {
+ updatePreviewPane( tab );
+ }
+ );
+ }
+
+ /**
+ * Called whenever the preview pane becomes out of sync with the file editor
+ * tab. This can be called when the text changes, the caret paragraph changes,
+ * or the file tab changes.
+ *
+ * @param tab The file editor tab that has been changed in some fashion.
+ */
+ private void updatePreviewPane( final FileEditorTab tab ) {
+ // TODO: Use a factory based on the filename extension. The default
+ // extension will be for a markdown file (e.g., on file new).
+ final Processor<String> hpp = new HTMLPreviewProcessor( getPreviewPane() );
+ final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp );
+ final Processor<String> mp = new MarkdownProcessor( mcrp );
+ final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, tab.getCaretPosition() );
+ final Processor<String> vp = new VariableProcessor( mcip, getResolvedMap() );
+
+ vp.processChain( tab.getEditorText() );
+ }
+
+ /**
+ * Called when the tab has changed to a new editor to replace the current
+ * definition pane with the
+ *
+ * @param tab
+ */
+ private void updateDefinitionPane( final FileEditorTab tab ) {
+ System.out.println( "load YAML for: " + tab.getPath() );
+ }
+
+ private void initVariableNameInjector() {
+ setVariableNameInjector(
+ new VariableNameInjector( getFileEditorPane(), getDefinitionPane() )
+ );
+ }
+
+ private Window getWindow() {
+ return getScene().getWindow();
+ }
+
+ public Scene getScene() {
+ return this.scene;
+ }
+
+ private void setScene( Scene scene ) {
+ this.scene = scene;
+ }
+
+ /**
+ * Creates a boolean property that is bound to another boolean value of the
+ * active editor.
+ */
+ private BooleanProperty createActiveBooleanProperty(
+ final Function<FileEditorTab, ObservableBooleanValue> func ) {
+
+ final BooleanProperty b = new SimpleBooleanProperty();
+ final FileEditorTab tab = getActiveFileEditor();
+
+ if( tab != null ) {
+ b.bind( func.apply( tab ) );
+ }
+
+ getFileEditorPane().activeFileEditorProperty().addListener(
+ (observable, oldFileEditor, newFileEditor) -> {
+ b.unbind();
+
+ if( newFileEditor != null ) {
+ b.bind( func.apply( newFileEditor ) );
+ } else {
+ b.set( false );
+ }
+ }
+ );
+
+ return b;
+ }
+
+ //---- File actions -------------------------------------------------------
+ private void fileNew() {
+ getFileEditorPane().newEditor();
+ }
+
+ private void fileOpen() {
+ getFileEditorPane().openFileDialog();
+ }
+
+ private void fileClose() {
+ getFileEditorPane().closeEditor( getActiveFileEditor(), true );
+ }
+
+ private void fileCloseAll() {
+ getFileEditorPane().closeAllEditors();
+ }
+
+ private void fileSave() {
+ getFileEditorPane().saveEditor( getActiveFileEditor() );
+ }
+
+ private void fileSaveAll() {
+ getFileEditorPane().saveAllEditors();
+ }
+
+ private void fileExit() {
+ final Window window = getWindow();
+ Event.fireEvent( window,
+ new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) );
+ }
+
+ //---- Help actions -------------------------------------------------------
+ private void helpAbout() {
+ Alert alert = new Alert( AlertType.INFORMATION );
+ alert.setTitle( Messages.get( "Dialog.about.title" ) );
+ alert.setHeaderText( Messages.get( "Dialog.about.header" ) );
+ alert.setContentText( Messages.get( "Dialog.about.content" ) );
+ alert.setGraphic( new ImageView( new Image( LOGO_32 ) ) );
+ alert.initOwner( getWindow() );
+
+ alert.showAndWait();
+ }
+
+ private FileEditorTabPane getFileEditorPane() {
+ if( this.fileEditorPane == null ) {
+ this.fileEditorPane = createFileEditorPane();
+ }
+
+ return this.fileEditorPane;
+ }
+
+ /**
+ * Create an editor pane to hold file editor tabs.
+ *
+ * @return A new instance, never null.
+ */
+ private FileEditorTabPane createFileEditorPane() {
+ return new FileEditorTabPane();
+ }
+
+ /**
+ * Reloads the preferences from the previous load.
+ */
+ private void restorePreferences() {
+ getFileEditorPane().restorePreferences();
+ }
+
+ private MarkdownEditorPane getActiveEditor() {
+ return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane());
+ }
+
+ private FileEditorTab getActiveFileEditor() {
+ return getFileEditorPane().getActiveFileEditor();
+ }
+
+ protected DefinitionPane createDefinitionPane() {
+ return new DefinitionPane( getTreeView() );
+ }
+
+ private DefinitionPane getDefinitionPane() {
+ if( this.definitionPane == null ) {
+ this.definitionPane = createDefinitionPane();
+ }
+
+ return this.definitionPane;
+ }
+
+ public MenuBar getMenuBar() {
+ return this.menuBar;
+ }
+
+ public void setMenuBar( MenuBar menuBar ) {
+ this.menuBar = menuBar;
+ }
+
+ public VariableNameInjector getVariableNameInjector() {
+ return this.variableNameInjector;
+ }
+
+ public void setVariableNameInjector( VariableNameInjector variableNameInjector ) {
+ this.variableNameInjector = variableNameInjector;
+ }
+
+ private float getFloat( final String key, final float defaultValue ) {
+ return getPreferences().getFloat( key, defaultValue );
+ }
+
+ private Preferences getPreferences() {
+ return getOptions().getState();
+ }
+
+ private Options getOptions() {
+ return this.options;
+ }
+
+ private synchronized TreeView<String> getTreeView() throws RuntimeException {
+ if( this.treeView == null ) {
+ try {
+ this.treeView = createTreeView();
+ } catch( IOException ex ) {
+
+ // TODO: Pop an error message.
+ throw new RuntimeException( ex );
+ }
+ }
+
+ return this.treeView;
+ }
+
+ private InputStream asStream( final String resource ) {
+ return getClass().getResourceAsStream( resource );
+ }
+
+ private TreeView<String> createTreeView() throws IOException {
+ // TODO: Associate variable file with path to current file.
+ return getYamlTreeAdapter().adapt(
+ asStream( "/com/scrivenvar/variables.yaml" ),
+ get( "Pane.defintion.node.root.title" )
+ );
+ }
+
+ private Map<String, String> getResolvedMap() {
+ return getYamlParser().createResolvedMap();
+ }
+
+ private YamlTreeAdapter getYamlTreeAdapter() {
+ if( this.yamlTreeAdapter == null ) {
+ setYamlTreeAdapter( new YamlTreeAdapter( getYamlParser() ) );
+ }
+
+ return this.yamlTreeAdapter;
+ }
+
+ private void setYamlTreeAdapter( final YamlTreeAdapter yamlTreeAdapter ) {
+ this.yamlTreeAdapter = yamlTreeAdapter;
+ }
+
+ private YamlParser getYamlParser() {
+ if( this.yamlParser == null ) {
+ setYamlParser( new YamlParser() );
+ }
+
+ return this.yamlParser;
+ }
+
+ private void setYamlParser( final YamlParser yamlParser ) {
+ this.yamlParser = yamlParser;
+ }
+
+ private synchronized HTMLPreviewPane getPreviewPane() {
+ if( this.previewPane == null ) {
+ this.previewPane = new HTMLPreviewPane();
+ }
+
+ return this.previewPane;
+ }
+
+ private Node createMenuBar() {
+ final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
+
+ // File actions
+ Action fileNewAction = new Action( Messages.get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
+ Action fileOpenAction = new Action( Messages.get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
+ Action fileCloseAction = new Action( Messages.get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
+ Action fileCloseAllAction = new Action( Messages.get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
+ Action fileSaveAction = new Action( Messages.get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
+ createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
+ Action fileSaveAllAction = new Action( Messages.get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
+ Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
+ Action fileExitAction = new Action( Messages.get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
+
+ // Edit actions
+ Action editUndoAction = new Action( Messages.get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
+ e -> getActiveEditor().undo(),
+ createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
+ Action editRedoAction = new Action( Messages.get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
+ e -> getActiveEditor().redo(),
+ createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
+
+ // Insert actions
+ Action insertBoldAction = new Action( Messages.get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
+ e -> getActiveEditor().surroundSelection( "**", "**" ),
+ activeFileEditorIsNull );
+ Action insertItalicAction = new Action( Messages.get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
+ e -> getActiveEditor().surroundSelection( "*", "*" ),
+ activeFileEditorIsNull );
+ Action insertStrikethroughAction = new Action( Messages.get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
+ e -> getActiveEditor().surroundSelection( "~~", "~~" ),
+ activeFileEditorIsNull );
+ Action insertBlockquoteAction = new Action( Messages.get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
+ e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
+ activeFileEditorIsNull );
+ Action insertCodeAction = new Action( Messages.get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
+ e -> getActiveEditor().surroundSelection( "`", "`" ),
+ activeFileEditorIsNull );
+ Action insertFencedCodeBlockAction = new Action( Messages.get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
+ e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", Messages.get( "Main.menu.insert.fenced_code_block.prompt" ) ),
+ activeFileEditorIsNull );
+
+ Action insertLinkAction = new Action( Messages.get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
+ e -> getActiveEditor().insertLink(),
+ activeFileEditorIsNull );
+ Action insertImageAction = new Action( Messages.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 = Messages.get( "Main.menu.insert.header_" + i );
+ final String accelerator = "Shortcut+" + i;
+ final String prompt = Messages.get( "Main.menu.insert.header_" + i + ".prompt" );
+
+ headers[ i - 1 ] = new Action( text, accelerator, HEADER,
+ e -> getActiveEditor().surroundSelection( markup, "", prompt ),
+ activeFileEditorIsNull );
+ }
+
+ Action insertUnorderedListAction = new Action( Messages.get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
+ e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
+ activeFileEditorIsNull );
+ Action insertOrderedListAction = new Action( Messages.get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
+ e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
+ activeFileEditorIsNull );
+ Action insertHorizontalRuleAction = new Action( Messages.get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
+ e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
+ activeFileEditorIsNull );
+
+ // Help actions
+ Action helpAboutAction = new Action( Messages.get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
+
+ //---- MenuBar ----
+ Menu fileMenu = ActionUtils.createMenu( Messages.get( "Main.menu.file" ),
+ fileNewAction,
+ fileOpenAction,
+ null,
+ fileCloseAction,
+ fileCloseAllAction,
+ null,
+ fileSaveAction,
+ fileSaveAllAction,
+ null,
+ fileExitAction );
+
+ Menu editMenu = ActionUtils.createMenu( Messages.get( "Main.menu.edit" ),
+ editUndoAction,
+ editRedoAction );
+
+ Menu insertMenu = ActionUtils.createMenu( Messages.get( "Main.menu.insert" ),
+ insertBoldAction,
+ insertItalicAction,
+ insertStrikethroughAction,
+ insertBlockquoteAction,
+ insertCodeAction,
+ insertFencedCodeBlockAction,
+ null,
+ insertLinkAction,
+ insertImageAction,
+ null,
+ headers[ 0 ],
+ headers[ 1 ],
+ headers[ 2 ],
+ headers[ 3 ],
+ headers[ 4 ],
+ headers[ 5 ],
+ null,
+ insertUnorderedListAction,
+ insertOrderedListAction,
+ insertHorizontalRuleAction );
+
+ Menu helpMenu = ActionUtils.createMenu( Messages.get( "Main.menu.help" ),
+ helpAboutAction );
+
+ menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu );
+
+ //---- ToolBar ----
+ ToolBar toolBar = ActionUtils.createToolBar(
+ fileNewAction,
+ fileOpenAction,
+ fileSaveAction,
+ null,
+ editUndoAction,
+ editRedoAction,
+ null,
+ insertBoldAction,
+ insertItalicAction,
+ insertBlockquoteAction,
+ insertCodeAction,
+ insertFencedCodeBlockAction,
+ null,
+ insertLinkAction,
+ insertImageAction,
+ null,
+ headers[ 0 ],
+ null,
+ insertUnorderedListAction,
+ insertOrderedListAction );
+
+ return new VBox( menuBar, toolBar );
+ }
}
src/main/java/com/scrivenvar/definition/DefinitionPane.java
*/
public class DefinitionPane extends AbstractPane {
-
+
private final static String TERMINALS = ":;,.!?-/\\¡¿";
-
+
private TreeView<String> treeView;
setTreeView( root );
initTreeView();
+ }
+
+ public void clear() {
+ getTreeView().setRoot( null );
}
final List<TreeItem<String>> branches = trunk.getChildren();
TreeItem<String> result = null;
-
+
for( final TreeItem<String> leaf : branches ) {
if( predicate.test( leaf.getValue() ) ) {
result = leaf;
break;
}
}
-
+
return result;
}
TreeItem<String> cItem = getTreeRoot();
TreeItem<String> pItem = cItem;
-
+
int index = path.indexOf( SEPARATOR );
-
+
while( index >= 0 ) {
final String node = path.substring( 0, index );
path = path.substring( index + 1 );
-
+
if( (cItem = findStartsNode( cItem, node )) == null ) {
break;
}
-
+
index = path.indexOf( SEPARATOR );
pItem = cItem;
cItem = pItem;
}
-
+
return sanitize( cItem );
}
final VariableTreeItem<String> root = getTreeRoot();
final VariableTreeItem<String> leaf = root.findLeaf( value );
-
+
return leaf == null
? root.findLeaf( rtrimTerminalPunctuation( value ) )
private String rtrimTerminalPunctuation( final String s ) {
final StringBuilder result = new StringBuilder( s.trim() );
-
+
while( TERMINALS.contains( "" + result.charAt( result.length() - 1 ) ) ) {
result.setLength( result.length() - 1 );
}
-
+
return result.toString();
}
? getFirst( item.getChildren() )
: item;
-
+
return result == null ? item : result;
}
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 );
}
}
-
+
private void initTreeView() {
getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
return getTreeView();
}
-
+
private MultipleSelectionModel getSelectionModel() {
return getTreeView().getSelectionModel();
return (VariableTreeItem<String>)getTreeView().getRoot();
}
-
+
public <T> boolean isRoot( final TreeItem<T> item ) {
return getTreeRoot().equals( item );
src/main/java/com/scrivenvar/editor/VariableNameInjector.java
import static org.fxmisc.wellbehaved.event.InputMap.consume;
import static org.fxmisc.wellbehaved.event.InputMap.sequence;
-import static com.scrivenvar.definition.Lists.getFirst;
-import static com.scrivenvar.definition.Lists.getLast;
-import static java.lang.Character.isSpaceChar;
-import static java.lang.Character.isWhitespace;
-import static java.lang.Math.min;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
-import static org.fxmisc.wellbehaved.event.InputMap.consume;
-import static com.scrivenvar.definition.Lists.getFirst;
-import static com.scrivenvar.definition.Lists.getLast;
-import static java.lang.Character.isSpaceChar;
-import static java.lang.Character.isWhitespace;
-import static java.lang.Math.min;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
-import static org.fxmisc.wellbehaved.event.InputMap.consume;
-import static com.scrivenvar.definition.Lists.getFirst;
-import static com.scrivenvar.definition.Lists.getLast;
-import static java.lang.Character.isSpaceChar;
-import static java.lang.Character.isWhitespace;
-import static java.lang.Math.min;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
-import static org.fxmisc.wellbehaved.event.InputMap.consume;
-import static com.scrivenvar.definition.Lists.getFirst;
-import static com.scrivenvar.definition.Lists.getLast;
-import static java.lang.Character.isSpaceChar;
-import static java.lang.Character.isWhitespace;
-import static java.lang.Math.min;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
-import static org.fxmisc.wellbehaved.event.InputMap.consume;
-import static com.scrivenvar.definition.Lists.getFirst;
-import static com.scrivenvar.definition.Lists.getLast;
-import static java.lang.Character.isSpaceChar;
-import static java.lang.Character.isWhitespace;
-import static java.lang.Math.min;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
-import static org.fxmisc.wellbehaved.event.InputMap.consume;
-import static com.scrivenvar.definition.Lists.getFirst;
-import static com.scrivenvar.definition.Lists.getLast;
-import static java.lang.Character.isSpaceChar;
-import static java.lang.Character.isWhitespace;
-import static java.lang.Math.min;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
-import static org.fxmisc.wellbehaved.event.InputMap.consume;
-import static com.scrivenvar.definition.Lists.getFirst;
-import static com.scrivenvar.definition.Lists.getLast;
-import static java.lang.Character.isSpaceChar;
-import static java.lang.Character.isWhitespace;
-import static java.lang.Math.min;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
-import static org.fxmisc.wellbehaved.event.InputMap.consume;
/**
src/main/java/com/scrivenvar/preview/HTMLPreviewPane.java
/**
+ * Clears out the HTML content from the preview.
+ */
+ public void clear() {
+ update( "" );
+ }
+
+ /**
* Scrolls to the caret position in the document.
*/
this.path = path;
}
-
+
/**
* Content to embed in a panel.
- *
+ *
* @return The content to display to the user.
*/
Delta1214 lines added, 1177 lines removed, 37-line increase