Dave Jarvis' Repositories

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

Started to create pipeline.

AuthorU-Dave-PC\Dave <email>
Date2016-11-17 22:37:42 GMT-0800
Commit2ac480c9f383fa69d501849e337f726c65645f8c
Parentd2e5afc
src/main/java/com/scrivendor/FileEditor.java
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;
src/main/java/com/scrivendor/FileEditorPane.java
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");
}
}
src/main/java/com/scrivendor/editor/MarkdownEditorPane.java
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);
+ });
}
}
src/main/java/com/scrivendor/editor/MarkdownSyntaxHighlighter.java
private void visitChildren( SuperNode node ) {
- for( Node child : node.getChildren() ) {
+ node.getChildren().stream().forEach((child) -> {
child.accept( this );
- }
+ });
}
src/main/java/com/scrivendor/preview/MarkdownPreviewPane.java
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;
}
}
src/main/java/com/scrivendor/service/DocumentProcessor.java
/**
+ * 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);
}
src/main/java/com/scrivendor/service/DocumentProcessorFactory.java
/**
+ * 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);
}
src/main/java/com/scrivendor/service/events/impl/DefaultDocumentProcessorFactory.java
* 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;
+
+ }
}
src/main/java/com/scrivendor/service/events/impl/FileType.java
+/*
+ * 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;
+ }
+}
src/main/java/com/scrivendor/service/impl/DefaultSettings.java
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;
}
src/main/resources/META-INF/services/com.scrivendor.service.DocumentProcessorFactory
-
+com.scrivendor.service.impl.DefaultDocumentProcessorFactory
src/main/resources/com/scrivendor/settings.properties
# 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=*.*
Delta999 lines added, 956 lines removed, 43-line increase