| Author | U-Dave-PC\Dave <email> |
|---|---|
| Date | 2016-11-17 22:37:42 GMT-0800 |
| Commit | 2ac480c9f383fa69d501849e337f726c65645f8c |
| Parent | d2e5afc |
| editorPane.pathProperty().bind( path ); | ||
| - | ||
| load(); | ||
| // Clear undo history after first load. | ||
| editorPane.getUndoManager().forgetHistory(); | ||
| // bind preview to editor | ||
| previewPane.pathProperty().bind( pathProperty() ); | ||
| - previewPane.markdownASTProperty().bind( editorPane.markdownASTProperty() ); | ||
| previewPane.scrollYProperty().bind( editorPane.scrollYProperty() ); | ||
| previewPane.getNode() ); | ||
| tab.setContent( splitPane ); | ||
| - | ||
| + | ||
| + // Allow the Markdown Preview Pane to receive change events within the | ||
| + // editor. | ||
| + editorPane.addChangeListener(previewPane); | ||
| editorPane.requestFocus(); | ||
| } | ||
| if( filePath != null ) { | ||
| try { | ||
| - byte[] bytes = Files.readAllBytes( filePath ); | ||
| + final byte[] bytes = Files.readAllBytes( filePath ); | ||
| String markdown; | ||
| import java.nio.file.Path; | ||
| import java.util.ArrayList; | ||
| -import java.util.Arrays; | ||
| -import java.util.List; | ||
| -import java.util.function.Consumer; | ||
| -import java.util.prefs.Preferences; | ||
| -import javafx.beans.property.ReadOnlyBooleanProperty; | ||
| -import javafx.beans.property.ReadOnlyBooleanWrapper; | ||
| -import javafx.beans.property.ReadOnlyObjectProperty; | ||
| -import javafx.beans.property.ReadOnlyObjectWrapper; | ||
| -import javafx.beans.value.ChangeListener; | ||
| -import javafx.collections.ListChangeListener; | ||
| -import javafx.collections.ObservableList; | ||
| -import javafx.event.Event; | ||
| -import javafx.scene.Node; | ||
| -import javafx.scene.control.Alert; | ||
| -import javafx.scene.control.ButtonType; | ||
| -import javafx.scene.control.Tab; | ||
| -import javafx.scene.control.TabPane; | ||
| -import javafx.scene.control.TabPane.TabClosingPolicy; | ||
| -import javafx.scene.input.InputEvent; | ||
| -import javafx.stage.FileChooser; | ||
| -import javafx.stage.FileChooser.ExtensionFilter; | ||
| -import org.fxmisc.richtext.StyledTextArea; | ||
| -import org.fxmisc.wellbehaved.event.EventPattern; | ||
| -import org.fxmisc.wellbehaved.event.InputMap; | ||
| - | ||
| -/** | ||
| - * Tab pane for file editors. | ||
| - * | ||
| - * @author Karl Tauber | ||
| - */ | ||
| -public class FileEditorPane extends AbstractPane { | ||
| - | ||
| - private final static List<String> DEFAULT_EXTENSIONS_MARKDOWN = Arrays.asList( | ||
| - "*.md", "*.markdown", "*.txt" ); | ||
| - | ||
| - private final static List<String> DEFAULT_EXTENSIONS_ALL = Arrays.asList( | ||
| - "*.*" ); | ||
| - | ||
| - private final static List<String> DEFAULT_EXTENSIONS_DEFINITION = Arrays.asList( | ||
| - "*.yml", "*.yaml", "*.properties", "*.props" ); | ||
| - | ||
| - private final Settings settings = Services.load( Settings.class ); | ||
| - private final AlertService alertService = Services.load( AlertService.class ); | ||
| - | ||
| - private MainWindow mainWindow; | ||
| - private final TabPane tabPane; | ||
| - private final ReadOnlyObjectWrapper<FileEditor> activeFileEditor = new ReadOnlyObjectWrapper<>(); | ||
| - private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper(); | ||
| - | ||
| - FileEditorPane( MainWindow mainWindow ) { | ||
| - setMainWindow( mainWindow ); | ||
| - | ||
| - tabPane = new TabPane(); | ||
| - tabPane.setFocusTraversable( false ); | ||
| - tabPane.setTabClosingPolicy( TabClosingPolicy.ALL_TABS ); | ||
| - | ||
| - // update activeFileEditor property | ||
| - tabPane.getSelectionModel().selectedItemProperty().addListener( (observable, oldTab, newTab) -> { | ||
| - this.activeFileEditor.set( (newTab != null) ? (FileEditor)newTab.getUserData() : null ); | ||
| - } ); | ||
| - | ||
| - // update anyFileEditorModified property | ||
| - ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> { | ||
| - boolean modified = false; | ||
| - for( Tab tab : tabPane.getTabs() ) { | ||
| - if( ((FileEditor)tab.getUserData()).isModified() ) { | ||
| - modified = true; | ||
| - break; | ||
| - } | ||
| - } | ||
| - this.anyFileEditorModified.set( modified ); | ||
| - }; | ||
| - | ||
| - tabPane.getTabs().addListener( (ListChangeListener<Tab>)c -> { | ||
| - while( c.next() ) { | ||
| - if( c.wasAdded() ) { | ||
| - for( Tab tab : c.getAddedSubList() ) { | ||
| - ((FileEditor)tab.getUserData()).modifiedProperty().addListener( modifiedListener ); | ||
| - } | ||
| - } else if( c.wasRemoved() ) { | ||
| - for( Tab tab : c.getRemoved() ) { | ||
| - ((FileEditor)tab.getUserData()).modifiedProperty().removeListener( modifiedListener ); | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - // changes in the tabs may also change anyFileEditorModified property | ||
| - // (e.g. closed modified file) | ||
| - modifiedListener.changed( null, null, null ); | ||
| - } ); | ||
| - | ||
| - // re-open files | ||
| - restoreState(); | ||
| - } | ||
| - | ||
| - 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 ); | ||
| - } | ||
| - | ||
| - public void removeEventListener( final InputMap<InputEvent> map ) { | ||
| - getActiveFileEditor().removeEventListener( map ); | ||
| - } | ||
| - | ||
| - private MainWindow getMainWindow() { | ||
| - return this.mainWindow; | ||
| - } | ||
| - | ||
| - private void setMainWindow( MainWindow mainWindow ) { | ||
| - this.mainWindow = mainWindow; | ||
| - } | ||
| - | ||
| - Node getNode() { | ||
| - return this.tabPane; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Allows clients to manipulate the editor content directly. | ||
| - * | ||
| - * @return The text area for the active file editor. | ||
| - */ | ||
| - public StyledTextArea getEditor() { | ||
| - return getActiveFileEditor().getEditorPane().getEditor(); | ||
| - } | ||
| - | ||
| - FileEditor getActiveFileEditor() { | ||
| - return this.activeFileEditor.get(); | ||
| - } | ||
| - | ||
| - ReadOnlyObjectProperty<FileEditor> activeFileEditorProperty() { | ||
| - return this.activeFileEditor.getReadOnlyProperty(); | ||
| - } | ||
| - | ||
| - ReadOnlyBooleanProperty anyFileEditorModifiedProperty() { | ||
| - return this.anyFileEditorModified.getReadOnlyProperty(); | ||
| - } | ||
| - | ||
| - private FileEditor createFileEditor( Path path ) { | ||
| - final FileEditor fileEditor = new FileEditor( path ); | ||
| - fileEditor.getTab().setOnCloseRequest( e -> { | ||
| - if( !canCloseEditor( fileEditor ) ) { | ||
| - e.consume(); | ||
| - } | ||
| - } ); | ||
| - return fileEditor; | ||
| - } | ||
| - | ||
| - FileEditor newEditor() { | ||
| - final FileEditor fileEditor = createFileEditor( null ); | ||
| - Tab tab = fileEditor.getTab(); | ||
| - tabPane.getTabs().add( tab ); | ||
| - tabPane.getSelectionModel().select( tab ); | ||
| - return fileEditor; | ||
| - } | ||
| - | ||
| - FileEditor[] openEditor() { | ||
| - final FileChooser fileChooser | ||
| - = createFileChooser( Messages.get( "Dialog.file.choose.open.title" ) ); | ||
| - final List<File> selectedFiles | ||
| - = fileChooser.showOpenMultipleDialog( getMainWindow().getScene().getWindow() ); | ||
| - | ||
| - if( selectedFiles == null ) { | ||
| - return null; | ||
| - } | ||
| - | ||
| - saveLastDirectory( selectedFiles.get( 0 ) ); | ||
| - return openEditors( selectedFiles, 0 ); | ||
| - } | ||
| - | ||
| - FileEditor[] openEditors( List<File> files, int activeIndex ) { | ||
| - // close single unmodified "Untitled" tab | ||
| - if( tabPane.getTabs().size() == 1 ) { | ||
| - FileEditor fileEditor = (FileEditor)tabPane.getTabs().get( 0 ).getUserData(); | ||
| - if( fileEditor.getPath() == null && !fileEditor.isModified() ) { | ||
| - closeEditor( fileEditor, false ); | ||
| - } | ||
| - } | ||
| - | ||
| - FileEditor[] fileEditors = new FileEditor[ files.size() ]; | ||
| - for( int i = 0; i < files.size(); i++ ) { | ||
| - Path path = files.get( i ).toPath(); | ||
| - | ||
| - // check whether file is already opened | ||
| - FileEditor fileEditor = findEditor( path ); | ||
| - if( fileEditor == null ) { | ||
| - fileEditor = createFileEditor( path ); | ||
| - | ||
| - tabPane.getTabs().add( fileEditor.getTab() ); | ||
| - } | ||
| - | ||
| - // select first file | ||
| - if( i == activeIndex ) { | ||
| - tabPane.getSelectionModel().select( fileEditor.getTab() ); | ||
| - } | ||
| - | ||
| - fileEditors[ i ] = fileEditor; | ||
| - } | ||
| - return fileEditors; | ||
| - } | ||
| - | ||
| - boolean saveEditor( FileEditor fileEditor ) { | ||
| - if( fileEditor == null || !fileEditor.isModified() ) { | ||
| - return true; | ||
| - } | ||
| - | ||
| - if( fileEditor.getPath() == null ) { | ||
| - tabPane.getSelectionModel().select( fileEditor.getTab() ); | ||
| - | ||
| - FileChooser fileChooser = createFileChooser( Messages.get( "Dialog.file.choose.save.title" ) ); | ||
| - File file = fileChooser.showSaveDialog( getMainWindow().getScene().getWindow() ); | ||
| - if( file == null ) { | ||
| - return false; | ||
| - } | ||
| - | ||
| - saveLastDirectory( file ); | ||
| - fileEditor.setPath( file.toPath() ); | ||
| - } | ||
| - | ||
| - return fileEditor.save(); | ||
| - } | ||
| - | ||
| - boolean saveAllEditors() { | ||
| - FileEditor[] allEditors = getAllEditors(); | ||
| - | ||
| - boolean success = true; | ||
| - for( FileEditor fileEditor : allEditors ) { | ||
| - if( !saveEditor( fileEditor ) ) { | ||
| - success = false; | ||
| - } | ||
| - } | ||
| - | ||
| - return success; | ||
| - } | ||
| - | ||
| - boolean canCloseEditor( final FileEditor fileEditor ) { | ||
| - if( !fileEditor.isModified() ) { | ||
| - return true; | ||
| - } | ||
| - | ||
| - final AlertMessage message = getAlertService().createAlertMessage( | ||
| - Messages.get( "Alert.file.close.title" ), | ||
| - Messages.get( "Alert.file.close.text" ), | ||
| - fileEditor.getTab().getText() | ||
| - ); | ||
| - | ||
| - final Alert alert = getAlertService().createAlertConfirmation( message ); | ||
| - final ButtonType response = alert.showAndWait().get(); | ||
| - | ||
| - return response == YES ? saveEditor( fileEditor ) : response == NO; | ||
| - } | ||
| - | ||
| - private AlertService getAlertService() { | ||
| - return this.alertService; | ||
| - } | ||
| - | ||
| - boolean closeEditor( FileEditor fileEditor, boolean save ) { | ||
| - if( fileEditor == null ) { | ||
| - return true; | ||
| - } | ||
| - | ||
| - final Tab tab = fileEditor.getTab(); | ||
| - | ||
| - if( save ) { | ||
| - Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT ); | ||
| - Event.fireEvent( tab, event ); | ||
| - if( event.isConsumed() ) { | ||
| - return false; | ||
| - } | ||
| - } | ||
| - | ||
| - tabPane.getTabs().remove( tab ); | ||
| - if( tab.getOnClosed() != null ) { | ||
| - Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) ); | ||
| - } | ||
| - | ||
| - return true; | ||
| - } | ||
| - | ||
| - boolean closeAllEditors() { | ||
| - FileEditor[] allEditors = getAllEditors(); | ||
| - FileEditor activeEditor = activeFileEditor.get(); | ||
| - | ||
| - // 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; | ||
| - } | ||
| - | ||
| - // save modified tabs | ||
| - for( int i = 0; i < allEditors.length; i++ ) { | ||
| - FileEditor fileEditor = allEditors[ i ]; | ||
| - if( fileEditor == activeEditor ) { | ||
| - continue; | ||
| - } | ||
| - | ||
| - if( fileEditor.isModified() ) { | ||
| - // activate the modified tab to make its modified content visible to the user | ||
| - tabPane.getSelectionModel().select( i ); | ||
| - | ||
| - if( !canCloseEditor( fileEditor ) ) { | ||
| - return false; | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - // Close all tabs. | ||
| - for( final FileEditor fileEditor : allEditors ) { | ||
| - if( !closeEditor( fileEditor, false ) ) { | ||
| - return false; | ||
| - } | ||
| - } | ||
| - | ||
| - saveState( allEditors, activeEditor ); | ||
| - | ||
| - return tabPane.getTabs().isEmpty(); | ||
| - } | ||
| - | ||
| - private FileEditor[] getAllEditors() { | ||
| - final ObservableList<Tab> tabs = tabPane.getTabs(); | ||
| - final FileEditor[] allEditors = new FileEditor[ tabs.size() ]; | ||
| - final int length = tabs.size(); | ||
| - | ||
| - for( int i = 0; i < length; i++ ) { | ||
| - allEditors[ i ] = (FileEditor)tabs.get( i ).getUserData(); | ||
| - } | ||
| - | ||
| - return allEditors; | ||
| - } | ||
| - | ||
| - private FileEditor findEditor( Path path ) { | ||
| - for( final Tab tab : tabPane.getTabs() ) { | ||
| - final FileEditor fileEditor = (FileEditor)tab.getUserData(); | ||
| - | ||
| - if( path.equals( fileEditor.getPath() ) ) { | ||
| - return fileEditor; | ||
| - } | ||
| - } | ||
| - | ||
| - return null; | ||
| - } | ||
| - | ||
| - private FileChooser createFileChooser( String title ) { | ||
| - final FileChooser fileChooser = new FileChooser(); | ||
| - | ||
| - fileChooser.setTitle( title ); | ||
| - fileChooser.getExtensionFilters().addAll( | ||
| - new ExtensionFilter( Messages.get( "Dialog.file.choose.filter.title.markdown" ), getMarkdownExtensions() ), | ||
| - new ExtensionFilter( Messages.get( "Dialog.file.choose.filter.title.definition" ), getDefinitionExtensions() ), | ||
| - new ExtensionFilter( Messages.get( "Dialog.file.choose.filter.title.all" ), getAllExtensions() ) ); | ||
| - | ||
| - 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 Settings getSettings() { | ||
| - return this.settings; | ||
| - } | ||
| - | ||
| - private List<String> getMarkdownExtensions() { | ||
| - return getStringSettingList( "Dialog.file.choose.filter.ext.markdown", DEFAULT_EXTENSIONS_MARKDOWN ); | ||
| - } | ||
| - | ||
| - private List<String> getDefinitionExtensions() { | ||
| - return getStringSettingList( "Dialog.file.choose.filter.ext.definition", DEFAULT_EXTENSIONS_DEFINITION ); | ||
| - } | ||
| - | ||
| - private List<String> getAllExtensions() { | ||
| - return getStringSettingList( "Dialog.file.choose.filter.ext.all", DEFAULT_EXTENSIONS_ALL ); | ||
| - } | ||
| - | ||
| - private List<String> getStringSettingList( String key, List<String> values ) { | ||
| - return getSettings().getStringSettingList( key, values ); | ||
| - } | ||
| - | ||
| - private void saveLastDirectory( File file ) { | ||
| - getState().put( "lastDirectory", file.getParent() ); | ||
| - } | ||
| - | ||
| - private void restoreState() { | ||
| - int activeIndex = 0; | ||
| - | ||
| - final Preferences state = getState(); | ||
| - final String[] fileNames = Utils.getPrefsStrings( state, "file" ); | ||
| - final String activeFileName = state.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 ); | ||
| - } | ||
| - | ||
| - private void saveState( final FileEditor[] allEditors, final FileEditor activeEditor ) { | ||
| - final ArrayList<String> fileNames = new ArrayList<>( allEditors.length ); | ||
| - | ||
| - for( final FileEditor fileEditor : allEditors ) { | ||
| - if( fileEditor.getPath() != null ) { | ||
| - fileNames.add( fileEditor.getPath().toString() ); | ||
| - } | ||
| - } | ||
| - | ||
| - final Preferences state = getState(); | ||
| - Utils.putPrefsStrings( state, "file", fileNames.toArray( new String[ fileNames.size() ] ) ); | ||
| - | ||
| - if( activeEditor != null && activeEditor.getPath() != null ) { | ||
| - state.put( "activeFile", activeEditor.getPath().toString() ); | ||
| - } else { | ||
| - state.remove( "activeFile" ); | ||
| +import java.util.List; | ||
| +import java.util.function.Consumer; | ||
| +import java.util.prefs.Preferences; | ||
| +import javafx.beans.property.ReadOnlyBooleanProperty; | ||
| +import javafx.beans.property.ReadOnlyBooleanWrapper; | ||
| +import javafx.beans.property.ReadOnlyObjectProperty; | ||
| +import javafx.beans.property.ReadOnlyObjectWrapper; | ||
| +import javafx.beans.value.ChangeListener; | ||
| +import javafx.collections.ListChangeListener; | ||
| +import javafx.collections.ObservableList; | ||
| +import javafx.event.Event; | ||
| +import javafx.scene.Node; | ||
| +import javafx.scene.control.Alert; | ||
| +import javafx.scene.control.ButtonType; | ||
| +import javafx.scene.control.Tab; | ||
| +import javafx.scene.control.TabPane; | ||
| +import javafx.scene.control.TabPane.TabClosingPolicy; | ||
| +import javafx.scene.input.InputEvent; | ||
| +import javafx.stage.FileChooser; | ||
| +import javafx.stage.FileChooser.ExtensionFilter; | ||
| +import org.fxmisc.richtext.StyledTextArea; | ||
| +import org.fxmisc.wellbehaved.event.EventPattern; | ||
| +import org.fxmisc.wellbehaved.event.InputMap; | ||
| + | ||
| +/** | ||
| + * Tab pane for file editors. | ||
| + * | ||
| + * @author Karl Tauber | ||
| + */ | ||
| +public class FileEditorPane extends AbstractPane { | ||
| + | ||
| + private final Settings settings = Services.load(Settings.class); | ||
| + private final AlertService alertService = Services.load(AlertService.class); | ||
| + | ||
| + private MainWindow mainWindow; | ||
| + private final TabPane tabPane; | ||
| + private final ReadOnlyObjectWrapper<FileEditor> activeFileEditor = new ReadOnlyObjectWrapper<>(); | ||
| + private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper(); | ||
| + | ||
| + public FileEditorPane(MainWindow mainWindow) { | ||
| + setMainWindow(mainWindow); | ||
| + | ||
| + tabPane = new TabPane(); | ||
| + tabPane.setFocusTraversable(false); | ||
| + tabPane.setTabClosingPolicy(TabClosingPolicy.ALL_TABS); | ||
| + | ||
| + // update activeFileEditor property | ||
| + tabPane.getSelectionModel().selectedItemProperty().addListener((observable, oldTab, newTab) -> { | ||
| + this.activeFileEditor.set((newTab != null) ? (FileEditor) newTab.getUserData() : null); | ||
| + }); | ||
| + | ||
| + // update anyFileEditorModified property | ||
| + ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> { | ||
| + boolean modified = false; | ||
| + for (Tab tab : tabPane.getTabs()) { | ||
| + if (((FileEditor) tab.getUserData()).isModified()) { | ||
| + modified = true; | ||
| + break; | ||
| + } | ||
| + } | ||
| + this.anyFileEditorModified.set(modified); | ||
| + }; | ||
| + | ||
| + tabPane.getTabs().addListener((ListChangeListener<Tab>) c -> { | ||
| + while (c.next()) { | ||
| + if (c.wasAdded()) { | ||
| + c.getAddedSubList().stream().forEach((tab) -> { | ||
| + ((FileEditor) tab.getUserData()).modifiedProperty().addListener(modifiedListener); | ||
| + }); | ||
| + } else if (c.wasRemoved()) { | ||
| + c.getRemoved().stream().forEach((tab) -> { | ||
| + ((FileEditor) tab.getUserData()).modifiedProperty().removeListener(modifiedListener); | ||
| + }); | ||
| + } | ||
| + } | ||
| + | ||
| + // changes in the tabs may also change anyFileEditorModified property | ||
| + // (e.g. closed modified file) | ||
| + modifiedListener.changed(null, null, null); | ||
| + }); | ||
| + | ||
| + // re-open files | ||
| + restoreState(); | ||
| + } | ||
| + | ||
| + 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); | ||
| + } | ||
| + | ||
| + public void removeEventListener(final InputMap<InputEvent> map) { | ||
| + getActiveFileEditor().removeEventListener(map); | ||
| + } | ||
| + | ||
| + private MainWindow getMainWindow() { | ||
| + return this.mainWindow; | ||
| + } | ||
| + | ||
| + private void setMainWindow(MainWindow mainWindow) { | ||
| + this.mainWindow = mainWindow; | ||
| + } | ||
| + | ||
| + Node getNode() { | ||
| + return this.tabPane; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Allows clients to manipulate the editor content directly. | ||
| + * | ||
| + * @return The text area for the active file editor. | ||
| + */ | ||
| + public StyledTextArea getEditor() { | ||
| + return getActiveFileEditor().getEditorPane().getEditor(); | ||
| + } | ||
| + | ||
| + FileEditor getActiveFileEditor() { | ||
| + return this.activeFileEditor.get(); | ||
| + } | ||
| + | ||
| + ReadOnlyObjectProperty<FileEditor> activeFileEditorProperty() { | ||
| + return this.activeFileEditor.getReadOnlyProperty(); | ||
| + } | ||
| + | ||
| + ReadOnlyBooleanProperty anyFileEditorModifiedProperty() { | ||
| + return this.anyFileEditorModified.getReadOnlyProperty(); | ||
| + } | ||
| + | ||
| + private FileEditor createFileEditor(Path path) { | ||
| + final FileEditor fileEditor = new FileEditor(path); | ||
| + fileEditor.getTab().setOnCloseRequest(e -> { | ||
| + if (!canCloseEditor(fileEditor)) { | ||
| + e.consume(); | ||
| + } | ||
| + }); | ||
| + return fileEditor; | ||
| + } | ||
| + | ||
| + FileEditor newEditor() { | ||
| + final FileEditor fileEditor = createFileEditor(null); | ||
| + Tab tab = fileEditor.getTab(); | ||
| + tabPane.getTabs().add(tab); | ||
| + tabPane.getSelectionModel().select(tab); | ||
| + return fileEditor; | ||
| + } | ||
| + | ||
| + FileEditor[] openEditor() { | ||
| + final FileChooser fileChooser | ||
| + = createFileChooser(Messages.get("Dialog.file.choose.open.title")); | ||
| + final List<File> selectedFiles | ||
| + = fileChooser.showOpenMultipleDialog(getMainWindow().getScene().getWindow()); | ||
| + | ||
| + if (selectedFiles == null) { | ||
| + return null; | ||
| + } | ||
| + | ||
| + saveLastDirectory(selectedFiles.get(0)); | ||
| + return openEditors(selectedFiles, 0); | ||
| + } | ||
| + | ||
| + FileEditor[] openEditors(List<File> files, int activeIndex) { | ||
| + // close single unmodified "Untitled" tab | ||
| + if (tabPane.getTabs().size() == 1) { | ||
| + FileEditor fileEditor = (FileEditor) tabPane.getTabs().get(0).getUserData(); | ||
| + if (fileEditor.getPath() == null && !fileEditor.isModified()) { | ||
| + closeEditor(fileEditor, false); | ||
| + } | ||
| + } | ||
| + | ||
| + FileEditor[] fileEditors = new FileEditor[files.size()]; | ||
| + for (int i = 0; i < files.size(); i++) { | ||
| + Path path = files.get(i).toPath(); | ||
| + | ||
| + // check whether file is already opened | ||
| + FileEditor fileEditor = findEditor(path); | ||
| + if (fileEditor == null) { | ||
| + fileEditor = createFileEditor(path); | ||
| + | ||
| + tabPane.getTabs().add(fileEditor.getTab()); | ||
| + } | ||
| + | ||
| + // select first file | ||
| + if (i == activeIndex) { | ||
| + tabPane.getSelectionModel().select(fileEditor.getTab()); | ||
| + } | ||
| + | ||
| + fileEditors[i] = fileEditor; | ||
| + } | ||
| + return fileEditors; | ||
| + } | ||
| + | ||
| + boolean saveEditor(FileEditor fileEditor) { | ||
| + if (fileEditor == null || !fileEditor.isModified()) { | ||
| + return true; | ||
| + } | ||
| + | ||
| + if (fileEditor.getPath() == null) { | ||
| + tabPane.getSelectionModel().select(fileEditor.getTab()); | ||
| + | ||
| + FileChooser fileChooser = createFileChooser(Messages.get("Dialog.file.choose.save.title")); | ||
| + File file = fileChooser.showSaveDialog(getMainWindow().getScene().getWindow()); | ||
| + if (file == null) { | ||
| + return false; | ||
| + } | ||
| + | ||
| + saveLastDirectory(file); | ||
| + fileEditor.setPath(file.toPath()); | ||
| + } | ||
| + | ||
| + return fileEditor.save(); | ||
| + } | ||
| + | ||
| + boolean saveAllEditors() { | ||
| + FileEditor[] allEditors = getAllEditors(); | ||
| + | ||
| + boolean success = true; | ||
| + for (FileEditor fileEditor : allEditors) { | ||
| + if (!saveEditor(fileEditor)) { | ||
| + success = false; | ||
| + } | ||
| + } | ||
| + | ||
| + return success; | ||
| + } | ||
| + | ||
| + boolean canCloseEditor(final FileEditor fileEditor) { | ||
| + if (!fileEditor.isModified()) { | ||
| + return true; | ||
| + } | ||
| + | ||
| + final AlertMessage message = getAlertService().createAlertMessage( | ||
| + Messages.get("Alert.file.close.title"), | ||
| + Messages.get("Alert.file.close.text"), | ||
| + fileEditor.getTab().getText() | ||
| + ); | ||
| + | ||
| + final Alert alert = getAlertService().createAlertConfirmation(message); | ||
| + final ButtonType response = alert.showAndWait().get(); | ||
| + | ||
| + return response == YES ? saveEditor(fileEditor) : response == NO; | ||
| + } | ||
| + | ||
| + private AlertService getAlertService() { | ||
| + return this.alertService; | ||
| + } | ||
| + | ||
| + boolean closeEditor(FileEditor fileEditor, boolean save) { | ||
| + if (fileEditor == null) { | ||
| + return true; | ||
| + } | ||
| + | ||
| + final Tab tab = fileEditor.getTab(); | ||
| + | ||
| + if (save) { | ||
| + Event event = new Event(tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT); | ||
| + Event.fireEvent(tab, event); | ||
| + if (event.isConsumed()) { | ||
| + return false; | ||
| + } | ||
| + } | ||
| + | ||
| + tabPane.getTabs().remove(tab); | ||
| + if (tab.getOnClosed() != null) { | ||
| + Event.fireEvent(tab, new Event(Tab.CLOSED_EVENT)); | ||
| + } | ||
| + | ||
| + return true; | ||
| + } | ||
| + | ||
| + boolean closeAllEditors() { | ||
| + FileEditor[] allEditors = getAllEditors(); | ||
| + FileEditor activeEditor = activeFileEditor.get(); | ||
| + | ||
| + // 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; | ||
| + } | ||
| + | ||
| + // save modified tabs | ||
| + for (int i = 0; i < allEditors.length; i++) { | ||
| + FileEditor fileEditor = allEditors[i]; | ||
| + if (fileEditor == activeEditor) { | ||
| + continue; | ||
| + } | ||
| + | ||
| + if (fileEditor.isModified()) { | ||
| + // activate the modified tab to make its modified content visible to the user | ||
| + tabPane.getSelectionModel().select(i); | ||
| + | ||
| + if (!canCloseEditor(fileEditor)) { | ||
| + return false; | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + // Close all tabs. | ||
| + for (final FileEditor fileEditor : allEditors) { | ||
| + if (!closeEditor(fileEditor, false)) { | ||
| + return false; | ||
| + } | ||
| + } | ||
| + | ||
| + saveState(allEditors, activeEditor); | ||
| + | ||
| + return tabPane.getTabs().isEmpty(); | ||
| + } | ||
| + | ||
| + private FileEditor[] getAllEditors() { | ||
| + final ObservableList<Tab> tabs = tabPane.getTabs(); | ||
| + final FileEditor[] allEditors = new FileEditor[tabs.size()]; | ||
| + final int length = tabs.size(); | ||
| + | ||
| + for (int i = 0; i < length; i++) { | ||
| + allEditors[i] = (FileEditor) tabs.get(i).getUserData(); | ||
| + } | ||
| + | ||
| + return allEditors; | ||
| + } | ||
| + | ||
| + private FileEditor findEditor(Path path) { | ||
| + for (final Tab tab : tabPane.getTabs()) { | ||
| + final FileEditor fileEditor = (FileEditor) tab.getUserData(); | ||
| + | ||
| + if (path.equals(fileEditor.getPath())) { | ||
| + return fileEditor; | ||
| + } | ||
| + } | ||
| + | ||
| + return null; | ||
| + } | ||
| + | ||
| + private FileChooser createFileChooser(String title) { | ||
| + final FileChooser fileChooser = new FileChooser(); | ||
| + | ||
| + fileChooser.setTitle(title); | ||
| + fileChooser.getExtensionFilters().addAll( | ||
| + new ExtensionFilter(Messages.get("Dialog.file.choose.filter.title.markdown"), getMarkdownExtensions()), | ||
| + new ExtensionFilter(Messages.get("Dialog.file.choose.filter.title.definition"), getDefinitionExtensions()), | ||
| + new ExtensionFilter(Messages.get("Dialog.file.choose.filter.title.xml"), getXMLExtensions()), | ||
| + new ExtensionFilter(Messages.get("Dialog.file.choose.filter.title.all"), getAllExtensions())); | ||
| + | ||
| + 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 Settings getSettings() { | ||
| + return this.settings; | ||
| + } | ||
| + | ||
| + private List<String> getMarkdownExtensions() { | ||
| + return getStringSettingList("Dialog.file.choose.filter.ext.markdown"); | ||
| + } | ||
| + | ||
| + private List<String> getDefinitionExtensions() { | ||
| + return getStringSettingList("Dialog.file.choose.filter.ext.definition"); | ||
| + } | ||
| + | ||
| + private List<String> getXMLExtensions() { | ||
| + return getStringSettingList("Dialog.file.choose.filter.ext.xml"); | ||
| + } | ||
| + | ||
| + private List<String> getAllExtensions() { | ||
| + return getStringSettingList("Dialog.file.choose.filter.ext.all"); | ||
| + } | ||
| + | ||
| + 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(File file) { | ||
| + getState().put("lastDirectory", file.getParent()); | ||
| + } | ||
| + | ||
| + private void restoreState() { | ||
| + int activeIndex = 0; | ||
| + | ||
| + final Preferences state = getState(); | ||
| + final String[] fileNames = Utils.getPrefsStrings(state, "file"); | ||
| + final String activeFileName = state.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); | ||
| + } | ||
| + | ||
| + private void saveState(final FileEditor[] allEditors, final FileEditor activeEditor) { | ||
| + final ArrayList<String> fileNames = new ArrayList<>(allEditors.length); | ||
| + | ||
| + for (final FileEditor fileEditor : allEditors) { | ||
| + if (fileEditor.getPath() != null) { | ||
| + fileNames.add(fileEditor.getPath().toString()); | ||
| + } | ||
| + } | ||
| + | ||
| + final Preferences state = getState(); | ||
| + Utils.putPrefsStrings(state, "file", fileNames.toArray(new String[fileNames.size()])); | ||
| + | ||
| + if (activeEditor != null && activeEditor.getPath() != null) { | ||
| + state.put("activeFile", activeEditor.getPath().toString()); | ||
| + } else { | ||
| + state.remove("activeFile"); | ||
| } | ||
| } |
| import javafx.beans.property.ReadOnlyDoubleProperty; | ||
| import javafx.beans.property.ReadOnlyDoubleWrapper; | ||
| -import javafx.beans.property.ReadOnlyObjectProperty; | ||
| -import javafx.beans.property.ReadOnlyObjectWrapper; | ||
| -import javafx.beans.property.SimpleObjectProperty; | ||
| -import javafx.beans.value.ChangeListener; | ||
| -import javafx.beans.value.ObservableValue; | ||
| -import javafx.event.Event; | ||
| -import javafx.scene.Node; | ||
| -import javafx.scene.control.IndexRange; | ||
| -import javafx.scene.input.InputEvent; | ||
| -import static javafx.scene.input.KeyCode.ENTER; | ||
| -import javafx.scene.input.KeyEvent; | ||
| -import org.fxmisc.flowless.VirtualizedScrollPane; | ||
| -import org.fxmisc.richtext.StyleClassedTextArea; | ||
| -import org.fxmisc.undo.UndoManager; | ||
| -import org.fxmisc.wellbehaved.event.EventPattern; | ||
| -import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| -import org.fxmisc.wellbehaved.event.InputMap; | ||
| -import static org.fxmisc.wellbehaved.event.InputMap.consume; | ||
| -import org.fxmisc.wellbehaved.event.Nodes; | ||
| -import org.pegdown.PegDownProcessor; | ||
| -import org.pegdown.ast.RootNode; | ||
| - | ||
| -/** | ||
| - * Markdown editor pane. | ||
| - * | ||
| - * Uses pegdown (https://github.com/sirthias/pegdown) for styling the markdown | ||
| - * content within a text area. | ||
| - * | ||
| - * @author Karl Tauber, White Magic Software, Ltd. | ||
| - */ | ||
| -public class MarkdownEditorPane extends AbstractPane { | ||
| - | ||
| - private static final Pattern AUTO_INDENT_PATTERN = Pattern.compile( | ||
| - "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" ); | ||
| - | ||
| - /** | ||
| - * Set when entering variable edit mode; retrieved upon exiting. | ||
| - */ | ||
| - private InputMap<InputEvent> nodeMap; | ||
| - | ||
| - private PegDownProcessor pegDownProcessor; | ||
| - private StyleClassedTextArea editor; | ||
| - private VirtualizedScrollPane<StyleClassedTextArea> scrollPane; | ||
| - private String lineSeparator = getLineSeparator(); | ||
| - | ||
| - private final ReadOnlyObjectWrapper<RootNode> markdownAST = new ReadOnlyObjectWrapper<>(); | ||
| - private final ReadOnlyDoubleWrapper scrollY = new ReadOnlyDoubleWrapper(); | ||
| - private final ObjectProperty<Path> path = new SimpleObjectProperty<>(); | ||
| - | ||
| - public MarkdownEditorPane() { | ||
| - initEditor(); | ||
| - initScrollEventListener(); | ||
| - initOptionEventListener(); | ||
| - } | ||
| - | ||
| - private void initEditor() { | ||
| - final StyleClassedTextArea textArea = getEditor(); | ||
| - | ||
| - textArea.setWrapText( true ); | ||
| - textArea.getStyleClass().add( "markdown-editor" ); | ||
| - textArea.getStylesheets().add( STYLESHEET_EDITOR ); | ||
| - | ||
| - MarkdownEditorPane.this.addEventListener( keyPressed( ENTER ), this::enterPressed ); | ||
| - | ||
| - // TODO: Wait for implementation that allows cutting lines, not paragraphs. | ||
| -// addEventListener( keyPressed( X, SHORTCUT_DOWN ), this::cutLine ); | ||
| - | ||
| - textArea.textProperty().addListener( (observable, oldText, newText) -> { | ||
| - textChanged( newText ); | ||
| - } ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * This method adds listeners to editor events. | ||
| - * | ||
| - * @param <T> The event type. | ||
| - * @param <U> The consumer type for the given event type. | ||
| - * @param event The event of interest. | ||
| - * @param consumer The method to call when the event happens. | ||
| - */ | ||
| - public <T extends Event, U extends T> void addEventListener( | ||
| - final EventPattern<? super T, ? extends U> event, | ||
| - final Consumer<? super U> consumer ) { | ||
| - Nodes.addInputMap( getEditor(), consume( event, consumer ) ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * This method adds listeners to editor events that can be removed without | ||
| - * affecting the original listeners (i.e., the original lister is restored on | ||
| - * a call to removeEventListener). | ||
| - * | ||
| - * @param map The map of methods to events. | ||
| - */ | ||
| - @SuppressWarnings( "unchecked" ) | ||
| - public void addEventListener( final InputMap<InputEvent> map ) { | ||
| - this.nodeMap = (InputMap<InputEvent>)getInputMap(); | ||
| - Nodes.addInputMap( getEditor(), map ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the value for "org.fxmisc.wellbehaved.event.inputmap". | ||
| - * | ||
| - * @return An input map of input events. | ||
| - */ | ||
| - private Object getInputMap() { | ||
| - return getEditor().getProperties().get( getInputMapKey() ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the hashmap key entry for the input map. | ||
| - * | ||
| - * @return "org.fxmisc.wellbehaved.event.inputmap" | ||
| - */ | ||
| - private String getInputMapKey() { | ||
| - return "org.fxmisc.wellbehaved.event.inputmap"; | ||
| - } | ||
| - | ||
| - /** | ||
| - * This method removes listeners to editor events and restores the default | ||
| - * handler. | ||
| - * | ||
| - * @param map The map of methods to events. | ||
| - */ | ||
| - public void removeEventListener( final InputMap<InputEvent> map ) { | ||
| - Nodes.removeInputMap( getEditor(), map ); | ||
| - Nodes.addInputMap( getEditor(), this.nodeMap ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Add a listener to update the scrollY property. | ||
| - */ | ||
| - private void initScrollEventListener() { | ||
| - final StyleClassedTextArea textArea = getEditor(); | ||
| - | ||
| - ChangeListener<Double> scrollYListener = (observable, oldValue, newValue) -> { | ||
| - double value = textArea.estimatedScrollYProperty().getValue(); | ||
| - double maxValue = textArea.totalHeightEstimateProperty().getOrElse( 0. ) - textArea.getHeight(); | ||
| - scrollY.set( (maxValue > 0) ? Math.min( Math.max( value / maxValue, 0 ), 1 ) : 0 ); | ||
| - }; | ||
| - | ||
| - textArea.estimatedScrollYProperty().addListener( scrollYListener ); | ||
| - textArea.totalHeightEstimateProperty().addListener( scrollYListener ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Listen to option changes. | ||
| - */ | ||
| - private void initOptionEventListener() { | ||
| - StyleClassedTextArea textArea = getEditor(); | ||
| - | ||
| - InvalidationListener listener = e -> { | ||
| - if( textArea.getScene() == null ) { | ||
| - // Editor closed but not yet garbage collected. | ||
| - return; | ||
| - } | ||
| - | ||
| - // Re-process markdown if markdown extensions option changes. | ||
| - if( e == getOptions().markdownExtensionsProperty() ) { | ||
| - pegDownProcessor = null; | ||
| - textChanged( textArea.getText() ); | ||
| - } | ||
| - }; | ||
| - | ||
| - WeakInvalidationListener weakOptionsListener = new WeakInvalidationListener( listener ); | ||
| - getOptions().markdownExtensionsProperty().addListener( weakOptionsListener ); | ||
| - } | ||
| - | ||
| - private void setEditor( StyleClassedTextArea textArea ) { | ||
| - this.editor = textArea; | ||
| - } | ||
| - | ||
| - public synchronized StyleClassedTextArea getEditor() { | ||
| - if( this.editor == null ) { | ||
| - setEditor( createTextArea() ); | ||
| - } | ||
| - | ||
| - return this.editor; | ||
| - } | ||
| - | ||
| - protected StyleClassedTextArea createTextArea() { | ||
| - return new StyleClassedTextArea( false ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the scroll pane that contains the text area. | ||
| - * | ||
| - * @return | ||
| - */ | ||
| - public Node getNode() { | ||
| - if( this.scrollPane == null ) { | ||
| - this.scrollPane = createScrollPane(); | ||
| - } | ||
| - | ||
| - return scrollPane; | ||
| - } | ||
| - | ||
| - protected VirtualizedScrollPane<StyleClassedTextArea> createScrollPane() { | ||
| - return new VirtualizedScrollPane<>( getEditor() ); | ||
| - } | ||
| - | ||
| - public UndoManager getUndoManager() { | ||
| - return getEditor().getUndoManager(); | ||
| - } | ||
| - | ||
| - @Override | ||
| - public void requestFocus() { | ||
| - Platform.runLater( () -> getEditor().requestFocus() ); | ||
| - } | ||
| - | ||
| - private String getLineSeparator() { | ||
| - final String separator = getOptions().getLineSeparator(); | ||
| - return (separator != null) | ||
| - ? separator | ||
| - : System.getProperty( "line.separator", "\n" ); | ||
| - } | ||
| - | ||
| - private String determineLineSeparator( String str ) { | ||
| - int strLength = str.length(); | ||
| - for( int i = 0; i < strLength; i++ ) { | ||
| - char ch = str.charAt( i ); | ||
| - if( ch == '\n' ) { | ||
| - return (i > 0 && str.charAt( i - 1 ) == '\r') ? "\r\n" : "\n"; | ||
| - } | ||
| - } | ||
| - return getLineSeparator(); | ||
| - } | ||
| - | ||
| - public String getMarkdown() { | ||
| - String markdown = getEditor().getText(); | ||
| - if( !lineSeparator.equals( "\n" ) ) { | ||
| - markdown = markdown.replace( "\n", lineSeparator ); | ||
| - } | ||
| - return markdown; | ||
| - } | ||
| - | ||
| - public void setMarkdown( String markdown ) { | ||
| - lineSeparator = determineLineSeparator( markdown ); | ||
| - getEditor().replaceText( markdown ); | ||
| - getEditor().selectRange( 0, 0 ); | ||
| - } | ||
| - | ||
| - public ObservableValue<String> markdownProperty() { | ||
| - return getEditor().textProperty(); | ||
| - } | ||
| - | ||
| - public RootNode getMarkdownAST() { | ||
| - return markdownAST.get(); | ||
| - } | ||
| - | ||
| - public ReadOnlyObjectProperty<RootNode> markdownASTProperty() { | ||
| - return markdownAST.getReadOnlyProperty(); | ||
| - } | ||
| - | ||
| - public double getScrollY() { | ||
| - return scrollY.get(); | ||
| - } | ||
| - | ||
| - public ReadOnlyDoubleProperty scrollYProperty() { | ||
| - return scrollY.getReadOnlyProperty(); | ||
| - } | ||
| - | ||
| - public Path getPath() { | ||
| - return path.get(); | ||
| - } | ||
| - | ||
| - public void setPath( Path path ) { | ||
| - this.path.set( path ); | ||
| - } | ||
| - | ||
| - public ObjectProperty<Path> pathProperty() { | ||
| - return path; | ||
| - } | ||
| - | ||
| - private Path getParentPath() { | ||
| - Path parentPath = getPath(); | ||
| - return (parentPath != null) ? parentPath.getParent() : null; | ||
| - } | ||
| - | ||
| - private void textChanged( String newText ) { | ||
| - RootNode astRoot = parseMarkdown( newText ); | ||
| - applyHighlighting( astRoot ); | ||
| - markdownAST.set( astRoot ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * TODO: Change to interface so that other processors can be pipelined. | ||
| - * | ||
| - * @return | ||
| - */ | ||
| - private synchronized PegDownProcessor getPegDownProcessor() { | ||
| - if( this.pegDownProcessor == null ) { | ||
| - this.pegDownProcessor = createPegDownProcessor(); | ||
| - } | ||
| - | ||
| - return this.pegDownProcessor; | ||
| - } | ||
| - | ||
| - protected PegDownProcessor createPegDownProcessor() { | ||
| - return new PegDownProcessor( getOptions().getMarkdownExtensions() ); | ||
| - } | ||
| - | ||
| - private RootNode parseMarkdown( String text ) { | ||
| - return getPegDownProcessor().parseMarkdown( text.toCharArray() ); | ||
| - } | ||
| - | ||
| - private void applyHighlighting( RootNode astRoot ) { | ||
| - MarkdownSyntaxHighlighter.highlight( editor, astRoot ); | ||
| - } | ||
| - | ||
| - private void enterPressed( KeyEvent e ) { | ||
| - final String currentLine = getEditor().getText( getEditor().getCurrentParagraph() ); | ||
| - final Matcher matcher = AUTO_INDENT_PATTERN.matcher( currentLine ); | ||
| - | ||
| - String newText = "\n"; | ||
| - | ||
| - if( matcher.matches() ) { | ||
| - if( !matcher.group( 2 ).isEmpty() ) { | ||
| - // indent new line with same whitespace characters and list markers as current line | ||
| - newText = newText.concat( matcher.group( 1 ) ); | ||
| - } else { | ||
| - // current line contains only whitespace characters and list markers | ||
| - // --> empty current line | ||
| - final int caretPosition = getEditor().getCaretPosition(); | ||
| - getEditor().selectRange( caretPosition - currentLine.length(), caretPosition ); | ||
| - } | ||
| - } | ||
| - | ||
| - getEditor().replaceSelection( newText ); | ||
| - } | ||
| - | ||
| - public void undo() { | ||
| - getEditor().getUndoManager().undo(); | ||
| - } | ||
| - | ||
| - public void redo() { | ||
| - getEditor().getUndoManager().redo(); | ||
| - } | ||
| - | ||
| - public void surroundSelection( String leading, String trailing ) { | ||
| - surroundSelection( leading, trailing, null ); | ||
| - } | ||
| - | ||
| - public void surroundSelection( String leading, String trailing, String hint ) { | ||
| - // Note: not using getEditor().insertText() to insert leading and trailing | ||
| - // because this would add two changes to undo history | ||
| - IndexRange selection = getEditor().getSelection(); | ||
| - int start = selection.getStart(); | ||
| - int end = selection.getEnd(); | ||
| - | ||
| - String selectedText = getEditor().getSelectedText(); | ||
| - | ||
| - // remove leading and trailing whitespaces from selected text | ||
| - String trimmedSelectedText = selectedText.trim(); | ||
| - if( trimmedSelectedText.length() < selectedText.length() ) { | ||
| - start += selectedText.indexOf( trimmedSelectedText ); | ||
| - end = start + trimmedSelectedText.length(); | ||
| - } | ||
| - | ||
| - // remove leading whitespaces from leading text if selection starts at zero | ||
| - if( start == 0 ) { | ||
| - leading = Utils.ltrim( leading ); | ||
| - } | ||
| - | ||
| - // remove trailing whitespaces from trailing text if selection ends at text end | ||
| - if( end == getEditor().getLength() ) { | ||
| - trailing = Utils.rtrim( trailing ); | ||
| - } | ||
| - | ||
| - // remove leading line separators from leading text | ||
| - // if there are line separators before the selected text | ||
| - if( leading.startsWith( "\n" ) ) { | ||
| - for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) { | ||
| - if( !"\n".equals( getEditor().getText( i, i + 1 ) ) ) { | ||
| - break; | ||
| - } | ||
| - leading = leading.substring( 1 ); | ||
| - } | ||
| - } | ||
| - | ||
| - // remove trailing line separators from trailing or leading text | ||
| - // if there are line separators after the selected text | ||
| - boolean trailingIsEmpty = trailing.isEmpty(); | ||
| - String str = trailingIsEmpty ? leading : trailing; | ||
| - if( str.endsWith( "\n" ) ) { | ||
| - for( int i = end; i < getEditor().getLength() && str.endsWith( "\n" ); i++ ) { | ||
| - if( !"\n".equals( getEditor().getText( i, i + 1 ) ) ) { | ||
| - break; | ||
| - } | ||
| - str = str.substring( 0, str.length() - 1 ); | ||
| - } | ||
| - if( trailingIsEmpty ) { | ||
| - leading = str; | ||
| - } else { | ||
| - trailing = str; | ||
| - } | ||
| - } | ||
| - | ||
| - int selStart = start + leading.length(); | ||
| - int selEnd = end + leading.length(); | ||
| - | ||
| - // insert hint text if selection is empty | ||
| - if( hint != null && trimmedSelectedText.isEmpty() ) { | ||
| - trimmedSelectedText = hint; | ||
| - selEnd = selStart + hint.length(); | ||
| - } | ||
| - | ||
| - // prevent undo merging with previous text entered by user | ||
| - getEditor().getUndoManager().preventMerge(); | ||
| - | ||
| - // replace text and update selection | ||
| - getEditor().replaceText( start, end, leading + trimmedSelectedText + trailing ); | ||
| - getEditor().selectRange( selStart, selEnd ); | ||
| - } | ||
| - | ||
| - public void insertLink() { | ||
| - LinkDialog dialog = new LinkDialog( getNode().getScene().getWindow(), getParentPath() ); | ||
| - dialog.showAndWait().ifPresent( result -> { | ||
| - getEditor().replaceSelection( result ); | ||
| - } ); | ||
| - } | ||
| - | ||
| - public void insertImage() { | ||
| - ImageDialog dialog = new ImageDialog( getNode().getScene().getWindow(), getParentPath() ); | ||
| - dialog.showAndWait().ifPresent( result -> { | ||
| - getEditor().replaceSelection( result ); | ||
| - } ); | ||
| +import javafx.beans.property.SimpleObjectProperty; | ||
| +import javafx.beans.value.ChangeListener; | ||
| +import javafx.beans.value.ObservableValue; | ||
| +import javafx.event.Event; | ||
| +import javafx.scene.Node; | ||
| +import javafx.scene.control.IndexRange; | ||
| +import javafx.scene.input.InputEvent; | ||
| +import static javafx.scene.input.KeyCode.ENTER; | ||
| +import javafx.scene.input.KeyEvent; | ||
| +import org.fxmisc.flowless.VirtualizedScrollPane; | ||
| +import org.fxmisc.richtext.StyleClassedTextArea; | ||
| +import org.fxmisc.undo.UndoManager; | ||
| +import org.fxmisc.wellbehaved.event.EventPattern; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import org.fxmisc.wellbehaved.event.InputMap; | ||
| +import static org.fxmisc.wellbehaved.event.InputMap.consume; | ||
| +import org.fxmisc.wellbehaved.event.Nodes; | ||
| + | ||
| +/** | ||
| + * Markdown editor pane. | ||
| + * | ||
| + * Uses pegdown (https://github.com/sirthias/pegdown) for styling the markdown | ||
| + * content within a text area. | ||
| + * | ||
| + * @author Karl Tauber and White Magic Software, Ltd. | ||
| + */ | ||
| +public class MarkdownEditorPane extends AbstractPane { | ||
| + | ||
| + private static final Pattern AUTO_INDENT_PATTERN = Pattern.compile( | ||
| + "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)"); | ||
| + | ||
| + /** | ||
| + * Set when entering variable edit mode; retrieved upon exiting. | ||
| + */ | ||
| + private InputMap<InputEvent> nodeMap; | ||
| + | ||
| + private StyleClassedTextArea editor; | ||
| + private VirtualizedScrollPane<StyleClassedTextArea> scrollPane; | ||
| + private String lineSeparator = getLineSeparator(); | ||
| + | ||
| + private final ReadOnlyDoubleWrapper scrollY = new ReadOnlyDoubleWrapper(); | ||
| + private final ObjectProperty<Path> path = new SimpleObjectProperty<>(); | ||
| + | ||
| + public MarkdownEditorPane() { | ||
| + initEditor(); | ||
| + initScrollEventListener(); | ||
| + initOptionEventListener(); | ||
| + } | ||
| + | ||
| + private void initEditor() { | ||
| + final StyleClassedTextArea textArea = getEditor(); | ||
| + | ||
| + textArea.setWrapText(true); | ||
| + textArea.getStyleClass().add("markdown-editor"); | ||
| + textArea.getStylesheets().add(STYLESHEET_EDITOR); | ||
| + | ||
| + addEventListener(keyPressed(ENTER), this::enterPressed); | ||
| + | ||
| + // TODO: Wait for implementation that allows cutting lines, not paragraphs. | ||
| +// addEventListener( keyPressed( X, SHORTCUT_DOWN ), this::cutLine ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Call to hook into changes to the text area. | ||
| + * | ||
| + * @param listener The listener to receive editor change events. | ||
| + */ | ||
| + public void addChangeListener(ChangeListener<? super String> listener) { | ||
| + getEditor().textProperty().addListener(listener); | ||
| + } | ||
| + | ||
| + /** | ||
| + * This method adds listeners to editor events. | ||
| + * | ||
| + * @param <T> The event type. | ||
| + * @param <U> The consumer type for the given event type. | ||
| + * @param event The event of interest. | ||
| + * @param consumer The method to call when the event happens. | ||
| + */ | ||
| + public <T extends Event, U extends T> void addEventListener( | ||
| + final EventPattern<? super T, ? extends U> event, | ||
| + final Consumer<? super U> consumer) { | ||
| + Nodes.addInputMap(getEditor(), consume(event, consumer)); | ||
| + } | ||
| + | ||
| + /** | ||
| + * This method adds listeners to editor events that can be removed without | ||
| + * affecting the original listeners (i.e., the original lister is restored on | ||
| + * a call to removeEventListener). | ||
| + * | ||
| + * @param map The map of methods to events. | ||
| + */ | ||
| + @SuppressWarnings("unchecked") | ||
| + public void addEventListener(final InputMap<InputEvent> map) { | ||
| + this.nodeMap = (InputMap<InputEvent>) getInputMap(); | ||
| + Nodes.addInputMap(getEditor(), map); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the value for "org.fxmisc.wellbehaved.event.inputmap". | ||
| + * | ||
| + * @return An input map of input events. | ||
| + */ | ||
| + private Object getInputMap() { | ||
| + return getEditor().getProperties().get(getInputMapKey()); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the hashmap key entry for the input map. | ||
| + * | ||
| + * @return "org.fxmisc.wellbehaved.event.inputmap" | ||
| + */ | ||
| + private String getInputMapKey() { | ||
| + return "org.fxmisc.wellbehaved.event.inputmap"; | ||
| + } | ||
| + | ||
| + /** | ||
| + * This method removes listeners to editor events and restores the default | ||
| + * handler. | ||
| + * | ||
| + * @param map The map of methods to events. | ||
| + */ | ||
| + public void removeEventListener(final InputMap<InputEvent> map) { | ||
| + Nodes.removeInputMap(getEditor(), map); | ||
| + Nodes.addInputMap(getEditor(), this.nodeMap); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Add a listener to update the scrollY property. | ||
| + */ | ||
| + private void initScrollEventListener() { | ||
| + final StyleClassedTextArea textArea = getEditor(); | ||
| + | ||
| + ChangeListener<Double> scrollYListener = (observable, oldValue, newValue) -> { | ||
| + double value = textArea.estimatedScrollYProperty().getValue(); | ||
| + double maxValue = textArea.totalHeightEstimateProperty().getOrElse(0.) - textArea.getHeight(); | ||
| + scrollY.set((maxValue > 0) ? Math.min(Math.max(value / maxValue, 0), 1) : 0); | ||
| + }; | ||
| + | ||
| + textArea.estimatedScrollYProperty().addListener(scrollYListener); | ||
| + textArea.totalHeightEstimateProperty().addListener(scrollYListener); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Listen to option changes. | ||
| + */ | ||
| + private void initOptionEventListener() { | ||
| + final StyleClassedTextArea textArea = getEditor(); | ||
| + | ||
| + final InvalidationListener listener = e -> { | ||
| + if (textArea.getScene() == null) { | ||
| + // Editor closed but not yet garbage collected. | ||
| + return; | ||
| + } | ||
| + | ||
| + // Re-process markdown if markdown extensions option changes. | ||
| + if (e == getOptions().markdownExtensionsProperty()) { | ||
| + // TODO: Watch for invalidation events. | ||
| + //textChanged(textArea.getText()); | ||
| + } | ||
| + }; | ||
| + | ||
| + WeakInvalidationListener weakOptionsListener = new WeakInvalidationListener(listener); | ||
| + getOptions().markdownExtensionsProperty().addListener(weakOptionsListener); | ||
| + } | ||
| + | ||
| + private void setEditor(StyleClassedTextArea textArea) { | ||
| + this.editor = textArea; | ||
| + } | ||
| + | ||
| + public synchronized StyleClassedTextArea getEditor() { | ||
| + if (this.editor == null) { | ||
| + setEditor(createTextArea()); | ||
| + } | ||
| + | ||
| + return this.editor; | ||
| + } | ||
| + | ||
| + protected StyleClassedTextArea createTextArea() { | ||
| + return new StyleClassedTextArea(false); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the scroll pane that contains the text area. | ||
| + * | ||
| + * @return | ||
| + */ | ||
| + public Node getNode() { | ||
| + if (this.scrollPane == null) { | ||
| + this.scrollPane = createScrollPane(); | ||
| + } | ||
| + | ||
| + return scrollPane; | ||
| + } | ||
| + | ||
| + protected VirtualizedScrollPane<StyleClassedTextArea> createScrollPane() { | ||
| + return new VirtualizedScrollPane<>(getEditor()); | ||
| + } | ||
| + | ||
| + public UndoManager getUndoManager() { | ||
| + return getEditor().getUndoManager(); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void requestFocus() { | ||
| + Platform.runLater(() -> getEditor().requestFocus()); | ||
| + } | ||
| + | ||
| + private String getLineSeparator() { | ||
| + final String separator = getOptions().getLineSeparator(); | ||
| + return (separator != null) | ||
| + ? separator | ||
| + : System.getProperty("line.separator", "\n"); | ||
| + } | ||
| + | ||
| + private String determineLineSeparator(String str) { | ||
| + int strLength = str.length(); | ||
| + for (int i = 0; i < strLength; i++) { | ||
| + char ch = str.charAt(i); | ||
| + if (ch == '\n') { | ||
| + return (i > 0 && str.charAt(i - 1) == '\r') ? "\r\n" : "\n"; | ||
| + } | ||
| + } | ||
| + return getLineSeparator(); | ||
| + } | ||
| + | ||
| + public String getMarkdown() { | ||
| + String markdown = getEditor().getText(); | ||
| + if (!lineSeparator.equals("\n")) { | ||
| + markdown = markdown.replace("\n", lineSeparator); | ||
| + } | ||
| + return markdown; | ||
| + } | ||
| + | ||
| + public void setMarkdown(String markdown) { | ||
| + lineSeparator = determineLineSeparator(markdown); | ||
| + getEditor().replaceText(markdown); | ||
| + getEditor().selectRange(0, 0); | ||
| + } | ||
| + | ||
| + public ObservableValue<String> markdownProperty() { | ||
| + return getEditor().textProperty(); | ||
| + } | ||
| + | ||
| + public double getScrollY() { | ||
| + return scrollY.get(); | ||
| + } | ||
| + | ||
| + public ReadOnlyDoubleProperty scrollYProperty() { | ||
| + return scrollY.getReadOnlyProperty(); | ||
| + } | ||
| + | ||
| + public Path getPath() { | ||
| + return path.get(); | ||
| + } | ||
| + | ||
| + public void setPath(Path path) { | ||
| + this.path.set(path); | ||
| + } | ||
| + | ||
| + public ObjectProperty<Path> pathProperty() { | ||
| + return path; | ||
| + } | ||
| + | ||
| + private Path getParentPath() { | ||
| + Path parentPath = getPath(); | ||
| + return (parentPath != null) ? parentPath.getParent() : null; | ||
| + } | ||
| + | ||
| + private void enterPressed(KeyEvent e) { | ||
| + final String currentLine = getEditor().getText(getEditor().getCurrentParagraph()); | ||
| + final Matcher matcher = AUTO_INDENT_PATTERN.matcher(currentLine); | ||
| + | ||
| + String newText = "\n"; | ||
| + | ||
| + if (matcher.matches()) { | ||
| + if (!matcher.group(2).isEmpty()) { | ||
| + // indent new line with same whitespace characters and list markers as current line | ||
| + newText = newText.concat(matcher.group(1)); | ||
| + } else { | ||
| + // current line contains only whitespace characters and list markers | ||
| + // --> empty current line | ||
| + final int caretPosition = getEditor().getCaretPosition(); | ||
| + getEditor().selectRange(caretPosition - currentLine.length(), caretPosition); | ||
| + } | ||
| + } | ||
| + | ||
| + getEditor().replaceSelection(newText); | ||
| + } | ||
| + | ||
| + public void undo() { | ||
| + getEditor().getUndoManager().undo(); | ||
| + } | ||
| + | ||
| + public void redo() { | ||
| + getEditor().getUndoManager().redo(); | ||
| + } | ||
| + | ||
| + public void surroundSelection(String leading, String trailing) { | ||
| + surroundSelection(leading, trailing, null); | ||
| + } | ||
| + | ||
| + public void surroundSelection(String leading, String trailing, String hint) { | ||
| + // Note: not using getEditor().insertText() to insert leading and trailing | ||
| + // because this would add two changes to undo history | ||
| + IndexRange selection = getEditor().getSelection(); | ||
| + int start = selection.getStart(); | ||
| + int end = selection.getEnd(); | ||
| + | ||
| + String selectedText = getEditor().getSelectedText(); | ||
| + | ||
| + // remove leading and trailing whitespaces from selected text | ||
| + String trimmedSelectedText = selectedText.trim(); | ||
| + if (trimmedSelectedText.length() < selectedText.length()) { | ||
| + start += selectedText.indexOf(trimmedSelectedText); | ||
| + end = start + trimmedSelectedText.length(); | ||
| + } | ||
| + | ||
| + // remove leading whitespaces from leading text if selection starts at zero | ||
| + if (start == 0) { | ||
| + leading = Utils.ltrim(leading); | ||
| + } | ||
| + | ||
| + // remove trailing whitespaces from trailing text if selection ends at text end | ||
| + if (end == getEditor().getLength()) { | ||
| + trailing = Utils.rtrim(trailing); | ||
| + } | ||
| + | ||
| + // remove leading line separators from leading text | ||
| + // if there are line separators before the selected text | ||
| + if (leading.startsWith("\n")) { | ||
| + for (int i = start - 1; i >= 0 && leading.startsWith("\n"); i--) { | ||
| + if (!"\n".equals(getEditor().getText(i, i + 1))) { | ||
| + break; | ||
| + } | ||
| + leading = leading.substring(1); | ||
| + } | ||
| + } | ||
| + | ||
| + // remove trailing line separators from trailing or leading text | ||
| + // if there are line separators after the selected text | ||
| + boolean trailingIsEmpty = trailing.isEmpty(); | ||
| + String str = trailingIsEmpty ? leading : trailing; | ||
| + if (str.endsWith("\n")) { | ||
| + for (int i = end; i < getEditor().getLength() && str.endsWith("\n"); i++) { | ||
| + if (!"\n".equals(getEditor().getText(i, i + 1))) { | ||
| + break; | ||
| + } | ||
| + str = str.substring(0, str.length() - 1); | ||
| + } | ||
| + if (trailingIsEmpty) { | ||
| + leading = str; | ||
| + } else { | ||
| + trailing = str; | ||
| + } | ||
| + } | ||
| + | ||
| + int selStart = start + leading.length(); | ||
| + int selEnd = end + leading.length(); | ||
| + | ||
| + // insert hint text if selection is empty | ||
| + if (hint != null && trimmedSelectedText.isEmpty()) { | ||
| + trimmedSelectedText = hint; | ||
| + selEnd = selStart + hint.length(); | ||
| + } | ||
| + | ||
| + // prevent undo merging with previous text entered by user | ||
| + getEditor().getUndoManager().preventMerge(); | ||
| + | ||
| + // replace text and update selection | ||
| + getEditor().replaceText(start, end, leading + trimmedSelectedText + trailing); | ||
| + getEditor().selectRange(selStart, selEnd); | ||
| + } | ||
| + | ||
| + public void insertLink() { | ||
| + LinkDialog dialog = new LinkDialog(getNode().getScene().getWindow(), getParentPath()); | ||
| + dialog.showAndWait().ifPresent(result -> { | ||
| + getEditor().replaceSelection(result); | ||
| + }); | ||
| + } | ||
| + | ||
| + public void insertImage() { | ||
| + ImageDialog dialog = new ImageDialog(getNode().getScene().getWindow(), getParentPath()); | ||
| + dialog.showAndWait().ifPresent(result -> { | ||
| + getEditor().replaceSelection(result); | ||
| + }); | ||
| } | ||
| } |
| private void visitChildren( SuperNode node ) { | ||
| - for( Node child : node.getChildren() ) { | ||
| + node.getChildren().stream().forEach((child) -> { | ||
| child.accept( this ); | ||
| - } | ||
| + }); | ||
| } | ||
| import java.nio.file.Path; | ||
| -import java.util.Collections; | ||
| import javafx.application.Platform; | ||
| import javafx.beans.property.DoubleProperty; | ||
| import javafx.beans.property.ObjectProperty; | ||
| import javafx.beans.property.SimpleDoubleProperty; | ||
| import javafx.beans.property.SimpleObjectProperty; | ||
| +import javafx.beans.value.ChangeListener; | ||
| +import javafx.beans.value.ObservableValue; | ||
| import javafx.scene.Node; | ||
| import javafx.scene.control.ScrollPane; | ||
| import static javafx.scene.control.ScrollPane.ScrollBarPolicy.ALWAYS; | ||
| import javafx.scene.web.WebEngine; | ||
| import javafx.scene.web.WebView; | ||
| -import org.pegdown.LinkRenderer; | ||
| -import org.pegdown.ToHtmlSerializer; | ||
| -import org.pegdown.VerbatimSerializer; | ||
| -import org.pegdown.ast.RootNode; | ||
| -import org.pegdown.plugins.PegDownPlugins; | ||
| +import org.commonmark.renderer.html.HtmlWriter; | ||
| /** | ||
| * Markdown preview pane. | ||
| * | ||
| - * @author Karl Tauber | ||
| + * @author Karl Tauber and White Magic Software, Ltd. | ||
| */ | ||
| -public final class MarkdownPreviewPane extends ScrollPane { | ||
| +public final class MarkdownPreviewPane extends ScrollPane implements ChangeListener { | ||
| - private final ObjectProperty<RootNode> markdownAST = new SimpleObjectProperty<>(); | ||
| private final ObjectProperty<Path> path = new SimpleObjectProperty<>(); | ||
| private final DoubleProperty scrollY = new SimpleDoubleProperty(); | ||
| private final WebView webView = new WebView(); | ||
| private int lastScrollX; | ||
| private int lastScrollY; | ||
| private boolean delayScroll; | ||
| + private String html; | ||
| public MarkdownPreviewPane() { | ||
| - setVbarPolicy( ALWAYS ); | ||
| - | ||
| - markdownASTProperty().addListener( (observable, oldValue, newValue) -> { | ||
| - update(); | ||
| - } ); | ||
| + setVbarPolicy(ALWAYS); | ||
| - pathProperty().addListener( (observable, oldValue, newValue) -> { | ||
| + pathProperty().addListener((observable, oldValue, newValue) -> { | ||
| update(); | ||
| - } ); | ||
| + }); | ||
| - scrollYProperty().addListener( (observable, oldValue, newValue) -> { | ||
| + scrollYProperty().addListener((observable, oldValue, newValue) -> { | ||
| scrollY(); | ||
| - } ); | ||
| + }); | ||
| } | ||
| - | ||
| - private String toHtml() { | ||
| - final RootNode root = getMarkdownAST(); | ||
| - return root == null | ||
| - ? "" | ||
| - : new ToHtmlSerializer( new LinkRenderer(), | ||
| - Collections.<String, VerbatimSerializer>emptyMap(), | ||
| - PegDownPlugins.NONE.getHtmlSerializerPlugins() ).toHtml( root ); | ||
| + @Override | ||
| + public void changed(ObservableValue observable, Object oldValue, Object newValue) { | ||
| + final StringBuilder sb = new StringBuilder(); | ||
| + final HtmlWriter writer = new HtmlWriter(sb); | ||
| + writer.text(newValue == null ? "" : newValue.toString()); | ||
| + setHtml(sb.toString()); | ||
| + update(); | ||
| } | ||
| - public void update() { | ||
| - if( !getEngine().getLoadWorker().isRunning() ) { | ||
| + private void update() { | ||
| + if (!getEngine().getLoadWorker().isRunning()) { | ||
| setScrollXY(); | ||
| } | ||
| getEngine().loadContent( | ||
| "<!DOCTYPE html>" | ||
| + "<html>" | ||
| + "<head>" | ||
| - + "<link rel='stylesheet' href='" + getClass().getResource( "markdownpad-github.css" ) + "'>" | ||
| + + "<link rel='stylesheet' href='" + getClass().getResource("markdownpad-github.css") + "'>" | ||
| + getBase() | ||
| + "</head>" | ||
| + "<body" + getScrollScript() + ">" | ||
| - + toHtml() | ||
| + + getHtml() | ||
| + "</body>" | ||
| - + "</html>" ); | ||
| + + "</html>"); | ||
| } | ||
| /** | ||
| * Obtain the window.scrollX and window.scrollY from web engine, but only no | ||
| * worker is running (in this case the result would be zero). | ||
| */ | ||
| private void setScrollXY() { | ||
| - lastScrollX = getNumber( execute( "window.scrollX" ) ); | ||
| - lastScrollY = getNumber( execute( "window.scrollY" ) ); | ||
| + lastScrollX = getNumber(execute("window.scrollX")); | ||
| + lastScrollY = getNumber(execute("window.scrollY")); | ||
| } | ||
| - private int getNumber( final Object number ) { | ||
| - return (number instanceof Number) ? ((Number)number).intValue() : 0; | ||
| + private int getNumber(final Object number) { | ||
| + return (number instanceof Number) ? ((Number) number).intValue() : 0; | ||
| } | ||
| */ | ||
| private void scrollY() { | ||
| - if( !delayScroll ) { | ||
| + if (!delayScroll) { | ||
| delayScroll = true; | ||
| - Platform.runLater( () -> { | ||
| + Platform.runLater(() -> { | ||
| delayScroll = false; | ||
| - scrollY( getScrollY() ); | ||
| - } ); | ||
| + scrollY(getScrollY()); | ||
| + }); | ||
| } | ||
| } | ||
| - private void scrollY( double value ) { | ||
| + private void scrollY(final double value) { | ||
| execute( | ||
| "window.scrollTo(0, (document.body.scrollHeight - window.innerHeight) * " | ||
| + value | ||
| - + ");" ); | ||
| + + ");"); | ||
| } | ||
| public Path getPath() { | ||
| return pathProperty().get(); | ||
| } | ||
| - public void setPath( Path path ) { | ||
| - pathProperty().set( path ); | ||
| + public void setPath(final Path path) { | ||
| + pathProperty().set(path); | ||
| } | ||
| public ObjectProperty<Path> pathProperty() { | ||
| return this.path; | ||
| - } | ||
| - | ||
| - public RootNode getMarkdownAST() { | ||
| - return markdownASTProperty().get(); | ||
| - } | ||
| - | ||
| - public void setMarkdownAST( RootNode astRoot ) { | ||
| - markdownASTProperty().set( astRoot ); | ||
| - } | ||
| - | ||
| - public ObjectProperty<RootNode> markdownASTProperty() { | ||
| - return this.markdownAST; | ||
| } | ||
| public double getScrollY() { | ||
| return scrollYProperty().get(); | ||
| } | ||
| - public void setScrollY( double value ) { | ||
| - scrollYProperty().set( value ); | ||
| + public void setScrollY(final double value) { | ||
| + scrollYProperty().set(value); | ||
| } | ||
| } | ||
| - private Object execute( String script ) { | ||
| - return getEngine().executeScript( script ); | ||
| + private Object execute(final String script) { | ||
| + return getEngine().executeScript(script); | ||
| } | ||
| private WebEngine getEngine() { | ||
| return getWebView().getEngine(); | ||
| } | ||
| private WebView getWebView() { | ||
| return this.webView; | ||
| + } | ||
| + | ||
| + private String getHtml() { | ||
| + return this.html == null ? "" : this.html; | ||
| + } | ||
| + | ||
| + private void setHtml(final String html) { | ||
| + this.html = html; | ||
| } | ||
| } | ||
| /** | ||
| + * Responsible for processing documents from one known format to another. | ||
| * | ||
| * @author White Magic Software, Ltd. | ||
| */ | ||
| -public interface Pipeline extends Service { | ||
| +public interface DocumentProcessor extends Service { | ||
| - public String process( String content ); | ||
| + /** | ||
| + * Adds a document processor to call after this processor finishes processing | ||
| + * the document given to the process method. | ||
| + * | ||
| + * @param next The processor that should transform the document after this | ||
| + * instance has finished processing. | ||
| + */ | ||
| + public void chain(DocumentProcessor next); | ||
| + | ||
| + /** | ||
| + * Processes the given content providing a transformation from one document | ||
| + * format into another. For example, this could convert from XML to structured | ||
| + * text (e.g., markdown) using an XSLT processor, followed by a markdown to | ||
| + * HTML processor (such as Common Mark). | ||
| + * | ||
| + * @param document The document to process. | ||
| + * @return The post-processed document. | ||
| + */ | ||
| + public String process(String document); | ||
| } | ||
| /** | ||
| + * Responsible for creating a chain of document processors for a given file | ||
| + * type. For example, an Rmd document creates an R document processor followed | ||
| + * by a markdown document processor to create a final web page document; whereas | ||
| + * an XML document creates an XSLT document processor with an option for | ||
| + * chaining another processor. | ||
| * | ||
| * @author White Magic Software, Ltd. | ||
| */ | ||
| -public interface Pipeline extends Service { | ||
| +public interface DocumentProcessorFactory extends Service { | ||
| - public String process( String content ); | ||
| + /** | ||
| + * Creates a document processor for a given file type. An XML document might | ||
| + * be associated with an XSLT processor, for example, letting the client code | ||
| + * chain a markdown processor afterwards. | ||
| + * | ||
| + * @param filetype The type of file to process (i.e., its extension). | ||
| + * @return A processor capable of transforming a document from the given filet | ||
| + * type to a destination file type (as hinted by the given file name | ||
| + * extension). | ||
| + */ | ||
| + public DocumentProcessor createDocumentProcessor(String filetype); | ||
| } | ||
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| */ | ||
| -package com.scrivendor.service; | ||
| +package com.scrivendor.service.events.impl; | ||
| + | ||
| +import com.scrivendor.service.*; | ||
| /** | ||
| + * Responsible for creating document processor (chains) for file types | ||
| + * (extensions). | ||
| * | ||
| * @author White Magic Software, Ltd. | ||
| */ | ||
| -public interface Pipeline extends Service { | ||
| +public class DefaultDocumentProcessorFactory implements Service { | ||
| - public String process( String content ); | ||
| + public DocumentProcessor createDocumentProcessor(String filetype){ | ||
| + if(filetype == null ) { | ||
| + filetype = "md"; | ||
| + } | ||
| + | ||
| + return null; | ||
| + | ||
| + } | ||
| } | ||
| +/* | ||
| + * To change this license header, choose License Headers in Project Properties. | ||
| + * To change this template file, choose Tools | Templates | ||
| + * and open the template in the editor. | ||
| + */ | ||
| +package com.scrivendor.service.events.impl; | ||
| + | ||
| +/** | ||
| + * Lists known file types for creating document processors via the factory. | ||
| + * | ||
| + * @author White Magic Software, Ltd. | ||
| + */ | ||
| +public enum FileType { | ||
| + MARKDOWN("md", "markdown", "mkdown", "mdown", "mkdn", "mkd", "mdwn", "mdtxt", "mdtext", "text", "txt"), | ||
| + R_MARKDOWN("Rmd"), | ||
| + XML("xml"); | ||
| + | ||
| + private final String[] extensions; | ||
| + | ||
| + private FileType(final String... extensions) { | ||
| + this.extensions = extensions; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns true if the given file type aligns with the extension for this | ||
| + * enumeration. | ||
| + * | ||
| + * @param filetype The file extension to compare against the internal list. | ||
| + * @return true The given filetype equals (case insensitive) the internal | ||
| + * type. | ||
| + */ | ||
| + public boolean isType(final String filetype) { | ||
| + boolean result = false; | ||
| + | ||
| + for (final String extension : this.extensions) { | ||
| + if (extension.equalsIgnoreCase(filetype)) { | ||
| + result = true; | ||
| + break; | ||
| + } | ||
| + } | ||
| + | ||
| + return result; | ||
| + } | ||
| +} | ||
| import java.net.URISyntaxException; | ||
| import java.net.URL; | ||
| +import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
| public DefaultSettings() | ||
| throws ConfigurationException, URISyntaxException, IOException { | ||
| - setProperties( createProperties() ); | ||
| + setProperties(createProperties()); | ||
| } | ||
| */ | ||
| @Override | ||
| - public String getSetting( String property, String defaultValue ) { | ||
| - return getSettings().getString( property, defaultValue ); | ||
| + public String getSetting(final String property, final String defaultValue) { | ||
| + return getSettings().getString(property, defaultValue); | ||
| } | ||
| */ | ||
| @Override | ||
| - public int getSetting( String property, int defaultValue ) { | ||
| - return getSettings().getInt( property, defaultValue ); | ||
| + public int getSetting(final String property, final int defaultValue) { | ||
| + return getSettings().getInt(property, defaultValue); | ||
| } | ||
| @Override | ||
| - public List<Object> getSettingList( String property, List<String> defaults ) { | ||
| - return getSettings().getList( property, defaults ); | ||
| + public List<Object> getSettingList(final String property, List<String> defaults) { | ||
| + if (defaults == null) { | ||
| + defaults = new ArrayList<>(); | ||
| + } | ||
| + | ||
| + return getSettings().getList(property, defaults); | ||
| } | ||
| */ | ||
| @Override | ||
| - public List<String> getStringSettingList( | ||
| - final String property, final List<String> defaults ) { | ||
| - final List<Object> settings = getSettingList( property, defaults ); | ||
| + public List<String> getStringSettingList( | ||
| + final String property, final List<String> defaults) { | ||
| + final List<Object> settings = getSettingList(property, defaults); | ||
| return settings.stream() | ||
| - .map( object -> Objects.toString( object, null ) ) | ||
| - .collect( Collectors.toList() ); | ||
| + .map(object -> Objects.toString(object, null)) | ||
| + .collect(Collectors.toList()); | ||
| } | ||
| private PropertiesConfiguration createProperties() | ||
| throws ConfigurationException { | ||
| final URL url = getPropertySource(); | ||
| return url == null | ||
| ? new PropertiesConfiguration() | ||
| - : new PropertiesConfiguration( url ); | ||
| + : new PropertiesConfiguration(url); | ||
| } | ||
| private URL getPropertySource() { | ||
| - return getClass().getResource( getSettingsFilename() ); | ||
| + return getClass().getResource(getSettingsFilename()); | ||
| } | ||
| private String getSettingsFilename() { | ||
| return SETTINGS_NAME; | ||
| } | ||
| - private void setProperties( final PropertiesConfiguration configuration ) { | ||
| + private void setProperties(final PropertiesConfiguration configuration) { | ||
| this.properties = configuration; | ||
| } | ||
| - | ||
| +com.scrivendor.service.impl.DefaultDocumentProcessorFactory |
| # Comma-separated list of filename extensions. | ||
| -Dialog.file.choose.filter.ext.markdown=*.Rmd,*.md,*.txt,*.markdown | ||
| +Dialog.file.choose.filter.ext.markdown=*.Rmd,*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt | ||
| Dialog.file.choose.filter.ext.definition=*.yml,*.yaml,*.properties,*.props | ||
| +Dialog.file.choose.filter.ext.xml=*.xml | ||
| Dialog.file.choose.filter.ext.all=*.* | ||
| Delta | 999 lines added, 956 lines removed, 43-line increase |
|---|