| Author | djarvis <email> |
|---|---|
| Date | 2016-12-11 01:00:38 GMT-0800 |
| Commit | 1efcf56fe0adaba7cce566d4d7da7978f93619ed |
| Parent | 64bbaed |
| import com.scrivenvar.editor.EditorPane; | ||
| import com.scrivenvar.editor.MarkdownEditorPane; | ||
| -import com.scrivenvar.preview.HTMLPreviewPane; | ||
| -import com.scrivenvar.service.Options; | ||
| -import com.scrivenvar.service.events.AlertMessage; | ||
| -import com.scrivenvar.service.events.AlertService; | ||
| -import java.nio.charset.Charset; | ||
| -import java.nio.file.Files; | ||
| -import java.nio.file.Path; | ||
| -import static java.util.Locale.ENGLISH; | ||
| -import java.util.function.Consumer; | ||
| -import javafx.application.Platform; | ||
| -import javafx.beans.binding.Bindings; | ||
| -import javafx.beans.property.BooleanProperty; | ||
| -import javafx.beans.property.ReadOnlyBooleanProperty; | ||
| -import javafx.beans.property.ReadOnlyBooleanWrapper; | ||
| -import javafx.beans.property.SimpleBooleanProperty; | ||
| -import javafx.event.Event; | ||
| -import javafx.scene.control.SplitPane; | ||
| -import javafx.scene.control.Tab; | ||
| -import javafx.scene.control.Tooltip; | ||
| -import javafx.scene.input.InputEvent; | ||
| -import javafx.scene.text.Text; | ||
| -import org.fxmisc.flowless.VirtualizedScrollPane; | ||
| -import org.fxmisc.richtext.StyleClassedTextArea; | ||
| -import org.fxmisc.undo.UndoManager; | ||
| -import org.fxmisc.wellbehaved.event.EventPattern; | ||
| -import org.fxmisc.wellbehaved.event.InputMap; | ||
| -import org.mozilla.universalchardet.UniversalDetector; | ||
| - | ||
| -/** | ||
| - * Editor for a single file. | ||
| - * | ||
| - * @author Karl Tauber and White Magic Software, Ltd. | ||
| - */ | ||
| -public final class FileEditorTab extends Tab { | ||
| - | ||
| - private final Options options = Services.load( Options.class ); | ||
| - private final AlertService alertService = Services.load( AlertService.class ); | ||
| - | ||
| - private EditorPane editorPane; | ||
| - private HTMLPreviewPane previewPane; | ||
| - | ||
| - /** | ||
| - * Character encoding used by the file (or default encoding if none found). | ||
| - */ | ||
| - private Charset encoding; | ||
| - | ||
| - private final ReadOnlyBooleanWrapper modified = new ReadOnlyBooleanWrapper(); | ||
| - private final BooleanProperty canUndo = new SimpleBooleanProperty(); | ||
| - private final BooleanProperty canRedo = new SimpleBooleanProperty(); | ||
| - private Path path; | ||
| - | ||
| - FileEditorTab( final Path path ) { | ||
| - setPath( path ); | ||
| - setUserData( this ); | ||
| - | ||
| - this.modified.addListener( (observable, oldPath, newPath) -> updateTab() ); | ||
| - updateTab(); | ||
| - | ||
| - setOnSelectionChanged( e -> { | ||
| - if( isSelected() ) { | ||
| - Platform.runLater( () -> activated() ); | ||
| - } | ||
| - } ); | ||
| - } | ||
| - | ||
| - private void updateTab() { | ||
| - setText( getTabTitle() ); | ||
| - setGraphic( getModifiedMark() ); | ||
| - setTooltip( getTabTooltip() ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the base filename (without the directory names). | ||
| - * | ||
| - * @return The untitled text if the path hasn't been set. | ||
| - */ | ||
| - private String getTabTitle() { | ||
| - final Path filePath = getPath(); | ||
| - | ||
| - return (filePath == null) | ||
| - ? Messages.get( "FileEditor.untitled" ) | ||
| - : filePath.getFileName().toString(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the full filename represented by the path. | ||
| - * | ||
| - * @return The untitled text if the path hasn't been set. | ||
| - */ | ||
| - private Tooltip getTabTooltip() { | ||
| - final Path filePath = getPath(); | ||
| - | ||
| - return (filePath == null) | ||
| - ? null | ||
| - : new Tooltip( filePath.toString() ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns a marker to indicate whether the file has been modified. | ||
| - * | ||
| - * @return "*" when the file has changed; otherwise null. | ||
| - */ | ||
| - private Text getModifiedMark() { | ||
| - return isModified() ? new Text( "*" ) : null; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Called when the user switches tab. | ||
| - */ | ||
| - private void activated() { | ||
| - // Tab is closed or no longer active. | ||
| - if( getTabPane() == null || !isSelected() ) { | ||
| - return; | ||
| - } | ||
| - | ||
| - // Switch to the tab without loading if the contents are already in memory. | ||
| - if( getContent() != null ) { | ||
| - getEditorPane().requestFocus(); | ||
| - return; | ||
| - } | ||
| - | ||
| - // Load the text and update the preview before the undo manager. | ||
| - load(); | ||
| - | ||
| - // Track undo requests (*must* be called after load). | ||
| - initUndoManager(); | ||
| - initSplitPane(); | ||
| - initFocus(); | ||
| - } | ||
| - | ||
| - public void initSplitPane() { | ||
| - final EditorPane editor = getEditorPane(); | ||
| - final HTMLPreviewPane preview = getPreviewPane(); | ||
| - final VirtualizedScrollPane<StyleClassedTextArea> editorScrollPane = editor.getScrollPane(); | ||
| - | ||
| - // Make the preview pane scroll correspond to the editor pane scroll. | ||
| - // Separate the edit and preview panels. | ||
| - setContent( new SplitPane( editorScrollPane, preview.getNode() ) ); | ||
| - } | ||
| - | ||
| - private void initFocus() { | ||
| - getEditorPane().requestFocus(); | ||
| - } | ||
| - | ||
| - private void initUndoManager() { | ||
| - final UndoManager undoManager = getUndoManager(); | ||
| - | ||
| - // Clear undo history after first load. | ||
| - undoManager.forgetHistory(); | ||
| - | ||
| - // Bind the editor undo manager to the properties. | ||
| - modified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) ); | ||
| - canUndo.bind( undoManager.undoAvailableProperty() ); | ||
| - canRedo.bind( undoManager.redoAvailableProperty() ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the index into the text where the caret blinks happily away. | ||
| - * | ||
| - * @return A number from 0 to the editor's document text length. | ||
| - */ | ||
| - public int getCaretPosition() { | ||
| - return getEditorPane().getEditor().getCaretPosition(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns true if the given path exactly matches this tab's path. | ||
| - * | ||
| - * @param check The path to compare against. | ||
| - * | ||
| - * @return true The paths are the same. | ||
| - */ | ||
| - public boolean isPath( final Path check ) { | ||
| - final Path filePath = getPath(); | ||
| - | ||
| - return filePath == null ? false : filePath.equals( check ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Reads the entire file contents from the path associated with this tab. | ||
| - */ | ||
| - private void load() { | ||
| - final Path filePath = getPath(); | ||
| - | ||
| - if( filePath != null ) { | ||
| - try { | ||
| - getEditorPane().setText( asString( Files.readAllBytes( filePath ) ) ); | ||
| - } catch( Exception ex ) { | ||
| - alert( | ||
| - "FileEditor.loadFailed.title", "FileEditor.loadFailed.message", ex | ||
| - ); | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - /** | ||
| - * Saves the entire file contents from the path associated with this tab. | ||
| - * | ||
| - * @return true The file has been saved. | ||
| - */ | ||
| - public boolean save() { | ||
| - try { | ||
| - Files.write( getPath(), asBytes( getEditorPane().getText() ) ); | ||
| - getEditorPane().getUndoManager().mark(); | ||
| - return true; | ||
| - } catch( Exception ex ) { | ||
| - return alert( | ||
| - "FileEditor.saveFailed.title", "FileEditor.saveFailed.message", ex | ||
| - ); | ||
| - } | ||
| - } | ||
| - | ||
| - /** | ||
| - * Creates an alert dialog and waits for it to close. | ||
| - * | ||
| - * @param titleKey Resource bundle key for the alert dialog title. | ||
| - * @param messageKey Resource bundle key for the alert dialog message. | ||
| - * @param e The unexpected happening. | ||
| - * | ||
| - * @return false | ||
| - */ | ||
| - private boolean alert( String titleKey, String messageKey, Exception e ) { | ||
| - final AlertService service = getAlertService(); | ||
| - | ||
| - final AlertMessage message = service.createAlertMessage( | ||
| - Messages.get( titleKey ), | ||
| - Messages.get( messageKey ), | ||
| - getPath(), | ||
| - e.getMessage() | ||
| - ); | ||
| - | ||
| - service.createAlertError( message ).showAndWait(); | ||
| - return false; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns a best guess at the file encoding. If the encoding could not be | ||
| - * detected, this will return the default charset for the JVM. | ||
| - * | ||
| - * @param bytes The bytes to perform character encoding detection. | ||
| - * | ||
| - * @return The character encoding. | ||
| - */ | ||
| - private Charset detectEncoding( final byte[] bytes ) { | ||
| - final UniversalDetector detector = new UniversalDetector( null ); | ||
| - detector.handleData( bytes, 0, bytes.length ); | ||
| - detector.dataEnd(); | ||
| - | ||
| - final String charset = detector.getDetectedCharset(); | ||
| - final Charset charEncoding = charset == null | ||
| - ? Charset.defaultCharset() | ||
| - : Charset.forName( charset.toUpperCase( ENGLISH ) ); | ||
| - | ||
| - detector.reset(); | ||
| - | ||
| - return charEncoding; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Converts the given string to an array of bytes using the encoding that was | ||
| - * originally detected (if any) and associated with this file. | ||
| - * | ||
| - * @param text The text to convert into the original file encoding. | ||
| - * | ||
| - * @return A series of bytes ready for writing to a file. | ||
| - */ | ||
| - private byte[] asBytes( final String text ) { | ||
| - return text.getBytes( getEncoding() ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Converts the given bytes into a Java String. This will call setEncoding | ||
| - * with the encoding detected by the CharsetDetector. | ||
| - * | ||
| - * @param text The text of unknown character encoding. | ||
| - * | ||
| - * @return The text, in its auto-detected encoding, as a String. | ||
| - */ | ||
| - private String asString( final byte[] text ) { | ||
| - setEncoding( detectEncoding( text ) ); | ||
| - return new String( text, getEncoding() ); | ||
| - } | ||
| - | ||
| - Path getPath() { | ||
| - return this.path; | ||
| - } | ||
| - | ||
| - void setPath( final Path path ) { | ||
| - this.path = path; | ||
| - } | ||
| - | ||
| - public boolean isModified() { | ||
| - return this.modified.get(); | ||
| - } | ||
| - | ||
| - ReadOnlyBooleanProperty modifiedProperty() { | ||
| - return this.modified.getReadOnlyProperty(); | ||
| - } | ||
| - | ||
| - BooleanProperty canUndoProperty() { | ||
| - return this.canUndo; | ||
| - } | ||
| - | ||
| - BooleanProperty canRedoProperty() { | ||
| - return this.canRedo; | ||
| - } | ||
| - | ||
| - private UndoManager getUndoManager() { | ||
| - return getEditorPane().getUndoManager(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Forwards the request to the editor pane. | ||
| - * | ||
| - * @param <T> The type of event listener to add. | ||
| - * @param <U> The type of consumer to add. | ||
| - * @param event The event that should trigger updates to the listener. | ||
| - * @param consumer The listener to receive update events. | ||
| - */ | ||
| - public <T extends Event, U extends T> void addEventListener( | ||
| - final EventPattern<? super T, ? extends U> event, | ||
| - final Consumer<? super U> consumer ) { | ||
| - getEditorPane().addEventListener( event, consumer ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Forwards to the editor pane's listeners for keyboard events. | ||
| - * | ||
| - * @param map The new input map to replace the existing keyboard listener. | ||
| - */ | ||
| - public void addEventListener( final InputMap<InputEvent> map ) { | ||
| - getEditorPane().addEventListener( map ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Forwards to the editor pane's listeners for keyboard events. | ||
| - * | ||
| - * @param map The existing input map to remove from the keyboard listeners. | ||
| - */ | ||
| - public void removeEventListener( final InputMap<InputEvent> map ) { | ||
| - getEditorPane().removeEventListener( map ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the editor pane, or creates one if it doesn't yet exist. | ||
| - * | ||
| - * @return The editor pane, never null. | ||
| - */ | ||
| - protected EditorPane getEditorPane() { | ||
| - if( this.editorPane == null ) { | ||
| - this.editorPane = new MarkdownEditorPane(); | ||
| - } | ||
| - | ||
| - return this.editorPane; | ||
| - } | ||
| - | ||
| - private AlertService getAlertService() { | ||
| - return this.alertService; | ||
| - } | ||
| - | ||
| - private Options getOptions() { | ||
| - return this.options; | ||
| - } | ||
| - | ||
| - public HTMLPreviewPane getPreviewPane() { | ||
| - if( this.previewPane == null ) { | ||
| - this.previewPane = new HTMLPreviewPane( getPath() ); | ||
| - } | ||
| - | ||
| - return this.previewPane; | ||
| +import com.scrivenvar.service.Options; | ||
| +import com.scrivenvar.service.events.AlertMessage; | ||
| +import com.scrivenvar.service.events.AlertService; | ||
| +import java.nio.charset.Charset; | ||
| +import java.nio.file.Files; | ||
| +import java.nio.file.Path; | ||
| +import static java.util.Locale.ENGLISH; | ||
| +import java.util.function.Consumer; | ||
| +import javafx.application.Platform; | ||
| +import javafx.beans.binding.Bindings; | ||
| +import javafx.beans.property.BooleanProperty; | ||
| +import javafx.beans.property.ReadOnlyBooleanProperty; | ||
| +import javafx.beans.property.ReadOnlyBooleanWrapper; | ||
| +import javafx.beans.property.SimpleBooleanProperty; | ||
| +import javafx.event.Event; | ||
| +import javafx.scene.Node; | ||
| +import javafx.scene.control.Tab; | ||
| +import javafx.scene.control.Tooltip; | ||
| +import javafx.scene.input.InputEvent; | ||
| +import javafx.scene.text.Text; | ||
| +import org.fxmisc.undo.UndoManager; | ||
| +import org.fxmisc.wellbehaved.event.EventPattern; | ||
| +import org.fxmisc.wellbehaved.event.InputMap; | ||
| +import org.mozilla.universalchardet.UniversalDetector; | ||
| + | ||
| +/** | ||
| + * Editor for a single file. | ||
| + * | ||
| + * @author Karl Tauber and White Magic Software, Ltd. | ||
| + */ | ||
| +public final class FileEditorTab extends Tab { | ||
| + | ||
| + private final Options options = Services.load( Options.class ); | ||
| + private final AlertService alertService = Services.load( AlertService.class ); | ||
| + | ||
| + private EditorPane editorPane; | ||
| + | ||
| + /** | ||
| + * Character encoding used by the file (or default encoding if none found). | ||
| + */ | ||
| + private Charset encoding; | ||
| + | ||
| + private final ReadOnlyBooleanWrapper modified = new ReadOnlyBooleanWrapper(); | ||
| + private final BooleanProperty canUndo = new SimpleBooleanProperty(); | ||
| + private final BooleanProperty canRedo = new SimpleBooleanProperty(); | ||
| + private Path path; | ||
| + | ||
| + FileEditorTab( final Path path ) { | ||
| + setPath( path ); | ||
| + setUserData( this ); | ||
| + | ||
| + this.modified.addListener( (observable, oldPath, newPath) -> updateTab() ); | ||
| + updateTab(); | ||
| + | ||
| + setOnSelectionChanged( e -> { | ||
| + if( isSelected() ) { | ||
| + Platform.runLater( () -> activated() ); | ||
| + } | ||
| + } ); | ||
| + } | ||
| + | ||
| + private void updateTab() { | ||
| + setText( getTabTitle() ); | ||
| + setGraphic( getModifiedMark() ); | ||
| + setTooltip( getTabTooltip() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the base filename (without the directory names). | ||
| + * | ||
| + * @return The untitled text if the path hasn't been set. | ||
| + */ | ||
| + private String getTabTitle() { | ||
| + final Path filePath = getPath(); | ||
| + | ||
| + return (filePath == null) | ||
| + ? Messages.get( "FileEditor.untitled" ) | ||
| + : filePath.getFileName().toString(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the full filename represented by the path. | ||
| + * | ||
| + * @return The untitled text if the path hasn't been set. | ||
| + */ | ||
| + private Tooltip getTabTooltip() { | ||
| + final Path filePath = getPath(); | ||
| + | ||
| + return (filePath == null) | ||
| + ? null | ||
| + : new Tooltip( filePath.toString() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns a marker to indicate whether the file has been modified. | ||
| + * | ||
| + * @return "*" when the file has changed; otherwise null. | ||
| + */ | ||
| + private Text getModifiedMark() { | ||
| + return isModified() ? new Text( "*" ) : null; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Called when the user switches tab. | ||
| + */ | ||
| + private void activated() { | ||
| + // Tab is closed or no longer active. | ||
| + if( getTabPane() == null || !isSelected() ) { | ||
| + return; | ||
| + } | ||
| + | ||
| + // Switch to the tab without loading if the contents are already in memory. | ||
| + if( getContent() != null ) { | ||
| + getEditorPane().requestFocus(); | ||
| + return; | ||
| + } | ||
| + | ||
| + // Load the text and update the preview before the undo manager. | ||
| + load(); | ||
| + | ||
| + // Track undo requests (*must* be called after load). | ||
| + initUndoManager(); | ||
| + initLayout(); | ||
| + initFocus(); | ||
| + } | ||
| + | ||
| + private void initLayout() { | ||
| + setContent( getScrollPane() ); | ||
| + } | ||
| + | ||
| + private Node getScrollPane() { | ||
| + return getEditorPane().getScrollPane(); | ||
| + } | ||
| + | ||
| + private void initFocus() { | ||
| + getEditorPane().requestFocus(); | ||
| + } | ||
| + | ||
| + private void initUndoManager() { | ||
| + final UndoManager undoManager = getUndoManager(); | ||
| + | ||
| + // Clear undo history after first load. | ||
| + undoManager.forgetHistory(); | ||
| + | ||
| + // Bind the editor undo manager to the properties. | ||
| + modified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) ); | ||
| + canUndo.bind( undoManager.undoAvailableProperty() ); | ||
| + canRedo.bind( undoManager.redoAvailableProperty() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the index into the text where the caret blinks happily away. | ||
| + * | ||
| + * @return A number from 0 to the editor's document text length. | ||
| + */ | ||
| + public int getCaretPosition() { | ||
| + return getEditorPane().getEditor().getCaretPosition(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns true if the given path exactly matches this tab's path. | ||
| + * | ||
| + * @param check The path to compare against. | ||
| + * | ||
| + * @return true The paths are the same. | ||
| + */ | ||
| + public boolean isPath( final Path check ) { | ||
| + final Path filePath = getPath(); | ||
| + | ||
| + return filePath == null ? false : filePath.equals( check ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Reads the entire file contents from the path associated with this tab. | ||
| + */ | ||
| + private void load() { | ||
| + final Path filePath = getPath(); | ||
| + | ||
| + if( filePath != null ) { | ||
| + try { | ||
| + getEditorPane().setText( asString( Files.readAllBytes( filePath ) ) ); | ||
| + } catch( Exception ex ) { | ||
| + alert( | ||
| + "FileEditor.loadFailed.title", "FileEditor.loadFailed.message", ex | ||
| + ); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + /** | ||
| + * Saves the entire file contents from the path associated with this tab. | ||
| + * | ||
| + * @return true The file has been saved. | ||
| + */ | ||
| + public boolean save() { | ||
| + try { | ||
| + Files.write( getPath(), asBytes( getEditorPane().getText() ) ); | ||
| + getEditorPane().getUndoManager().mark(); | ||
| + return true; | ||
| + } catch( Exception ex ) { | ||
| + return alert( | ||
| + "FileEditor.saveFailed.title", "FileEditor.saveFailed.message", ex | ||
| + ); | ||
| + } | ||
| + } | ||
| + | ||
| + /** | ||
| + * Creates an alert dialog and waits for it to close. | ||
| + * | ||
| + * @param titleKey Resource bundle key for the alert dialog title. | ||
| + * @param messageKey Resource bundle key for the alert dialog message. | ||
| + * @param e The unexpected happening. | ||
| + * | ||
| + * @return false | ||
| + */ | ||
| + private boolean alert( | ||
| + final String titleKey, final String messageKey, final Exception e ) { | ||
| + final AlertService service = getAlertService(); | ||
| + | ||
| + final AlertMessage message = service.createAlertMessage( | ||
| + Messages.get( titleKey ), | ||
| + Messages.get( messageKey ), | ||
| + getPath(), | ||
| + e.getMessage() | ||
| + ); | ||
| + | ||
| + service.createAlertError( message ).showAndWait(); | ||
| + return false; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns a best guess at the file encoding. If the encoding could not be | ||
| + * detected, this will return the default charset for the JVM. | ||
| + * | ||
| + * @param bytes The bytes to perform character encoding detection. | ||
| + * | ||
| + * @return The character encoding. | ||
| + */ | ||
| + private Charset detectEncoding( final byte[] bytes ) { | ||
| + final UniversalDetector detector = new UniversalDetector( null ); | ||
| + detector.handleData( bytes, 0, bytes.length ); | ||
| + detector.dataEnd(); | ||
| + | ||
| + final String charset = detector.getDetectedCharset(); | ||
| + final Charset charEncoding = charset == null | ||
| + ? Charset.defaultCharset() | ||
| + : Charset.forName( charset.toUpperCase( ENGLISH ) ); | ||
| + | ||
| + detector.reset(); | ||
| + | ||
| + return charEncoding; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Converts the given string to an array of bytes using the encoding that was | ||
| + * originally detected (if any) and associated with this file. | ||
| + * | ||
| + * @param text The text to convert into the original file encoding. | ||
| + * | ||
| + * @return A series of bytes ready for writing to a file. | ||
| + */ | ||
| + private byte[] asBytes( final String text ) { | ||
| + return text.getBytes( getEncoding() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Converts the given bytes into a Java String. This will call setEncoding | ||
| + * with the encoding detected by the CharsetDetector. | ||
| + * | ||
| + * @param text The text of unknown character encoding. | ||
| + * | ||
| + * @return The text, in its auto-detected encoding, as a String. | ||
| + */ | ||
| + private String asString( final byte[] text ) { | ||
| + setEncoding( detectEncoding( text ) ); | ||
| + return new String( text, getEncoding() ); | ||
| + } | ||
| + | ||
| + Path getPath() { | ||
| + return this.path; | ||
| + } | ||
| + | ||
| + void setPath( final Path path ) { | ||
| + this.path = path; | ||
| + } | ||
| + | ||
| + public boolean isModified() { | ||
| + return this.modified.get(); | ||
| + } | ||
| + | ||
| + ReadOnlyBooleanProperty modifiedProperty() { | ||
| + return this.modified.getReadOnlyProperty(); | ||
| + } | ||
| + | ||
| + BooleanProperty canUndoProperty() { | ||
| + return this.canUndo; | ||
| + } | ||
| + | ||
| + BooleanProperty canRedoProperty() { | ||
| + return this.canRedo; | ||
| + } | ||
| + | ||
| + private UndoManager getUndoManager() { | ||
| + return getEditorPane().getUndoManager(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Forwards the request to the editor pane. | ||
| + * | ||
| + * @param <T> The type of event listener to add. | ||
| + * @param <U> The type of consumer to add. | ||
| + * @param event The event that should trigger updates to the listener. | ||
| + * @param consumer The listener to receive update events. | ||
| + */ | ||
| + public <T extends Event, U extends T> void addEventListener( | ||
| + final EventPattern<? super T, ? extends U> event, | ||
| + final Consumer<? super U> consumer ) { | ||
| + getEditorPane().addEventListener( event, consumer ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Forwards to the editor pane's listeners for keyboard events. | ||
| + * | ||
| + * @param map The new input map to replace the existing keyboard listener. | ||
| + */ | ||
| + public void addEventListener( final InputMap<InputEvent> map ) { | ||
| + getEditorPane().addEventListener( map ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Forwards to the editor pane's listeners for keyboard events. | ||
| + * | ||
| + * @param map The existing input map to remove from the keyboard listeners. | ||
| + */ | ||
| + public void removeEventListener( final InputMap<InputEvent> map ) { | ||
| + getEditorPane().removeEventListener( map ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the editor pane, or creates one if it doesn't yet exist. | ||
| + * | ||
| + * @return The editor pane, never null. | ||
| + */ | ||
| + protected EditorPane getEditorPane() { | ||
| + if( this.editorPane == null ) { | ||
| + this.editorPane = new MarkdownEditorPane(); | ||
| + } | ||
| + | ||
| + return this.editorPane; | ||
| + } | ||
| + | ||
| + private AlertService getAlertService() { | ||
| + return this.alertService; | ||
| + } | ||
| + | ||
| + private Options getOptions() { | ||
| + return this.options; | ||
| } | ||
| * @author Karl Tauber and White Magic Software, Ltd. | ||
| */ | ||
| -public class FileEditorTabPane extends TabPane implements ChangeListener<Tab> { | ||
| - | ||
| - private final static String FILTER_PREFIX = "Dialog.file.choose.filter"; | ||
| - | ||
| - private final Options options = Services.load( Options.class ); | ||
| - private final Settings settings = Services.load( Settings.class ); | ||
| - private final AlertService alertService = Services.load( AlertService.class ); | ||
| - | ||
| - private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>(); | ||
| - private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper(); | ||
| - | ||
| - public FileEditorTabPane() { | ||
| - final ObservableList<Tab> tabs = getTabs(); | ||
| - | ||
| - setFocusTraversable( false ); | ||
| - setTabClosingPolicy( TabClosingPolicy.ALL_TABS ); | ||
| - | ||
| - // Observe the tab so that when a new tab is opened or selected, | ||
| - // a notification is kicked off. | ||
| - getSelectionModel().selectedItemProperty().addListener( this ); | ||
| - | ||
| - // update anyFileEditorModified property | ||
| - final ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> { | ||
| - for( final Tab tab : tabs ) { | ||
| - if( ((FileEditorTab)tab.getUserData()).isModified() ) { | ||
| - this.anyFileEditorModified.set( true ); | ||
| - break; | ||
| - } | ||
| - } | ||
| - }; | ||
| - | ||
| - tabs.addListener( (ListChangeListener<Tab>)change -> { | ||
| - while( change.next() ) { | ||
| - if( change.wasAdded() ) { | ||
| - change.getAddedSubList().stream().forEach( (tab) -> { | ||
| - ((FileEditorTab)tab.getUserData()).modifiedProperty().addListener( modifiedListener ); | ||
| - } ); | ||
| - } else if( change.wasRemoved() ) { | ||
| - change.getRemoved().stream().forEach( (tab) -> { | ||
| - ((FileEditorTab)tab.getUserData()).modifiedProperty().removeListener( modifiedListener ); | ||
| - } ); | ||
| - } | ||
| - } | ||
| - | ||
| - // Changes in the tabs may also change anyFileEditorModified property | ||
| - // (e.g. closed modified file) | ||
| - modifiedListener.changed( null, null, null ); | ||
| - } ); | ||
| - } | ||
| - | ||
| - public <T extends Event, U extends T> void addEventListener( | ||
| - final EventPattern<? super T, ? extends U> event, | ||
| - final Consumer<? super U> consumer ) { | ||
| - getActiveFileEditor().addEventListener( event, consumer ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Delegates to the active file editor pane, and, ultimately, to its text | ||
| - * area. | ||
| - * | ||
| - * @param map The map of methods to events. | ||
| - */ | ||
| - public void addEventListener( final InputMap<InputEvent> map ) { | ||
| - getActiveFileEditor().addEventListener( map ); | ||
| - } | ||
| - | ||
| - public void removeEventListener( final InputMap<InputEvent> map ) { | ||
| - getActiveFileEditor().removeEventListener( map ); | ||
| - } | ||
| - | ||
| - @Override | ||
| - public void changed( | ||
| - final ObservableValue<? extends Tab> observable, | ||
| - final Tab oldTab, | ||
| - final Tab newTab ) { | ||
| - | ||
| - if( newTab != null ) { | ||
| - this.activeFileEditor.set( (FileEditorTab)newTab.getUserData() ); | ||
| - } | ||
| - } | ||
| - | ||
| - Node getNode() { | ||
| - return this; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Allows clients to manipulate the editor content directly. | ||
| - * | ||
| - * @return The text area for the active file editor. | ||
| - */ | ||
| - public StyledTextArea getEditor() { | ||
| - return getActiveFileEditor().getEditorPane().getEditor(); | ||
| - } | ||
| - | ||
| - public FileEditorTab getActiveFileEditor() { | ||
| - return this.activeFileEditor.get(); | ||
| - } | ||
| - | ||
| - ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() { | ||
| - return this.activeFileEditor.getReadOnlyProperty(); | ||
| - } | ||
| - | ||
| - ReadOnlyBooleanProperty anyFileEditorModifiedProperty() { | ||
| - return this.anyFileEditorModified.getReadOnlyProperty(); | ||
| - } | ||
| - | ||
| - private FileEditorTab createFileEditor( final Path path ) { | ||
| - final FileEditorTab tab = new FileEditorTab( path ); | ||
| - | ||
| - tab.setOnCloseRequest( e -> { | ||
| - if( !canCloseEditor( tab ) ) { | ||
| - e.consume(); | ||
| - } | ||
| - } ); | ||
| - | ||
| - return tab; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Called when the user selects New from the File menu. | ||
| - * | ||
| - * @return The newly added tab. | ||
| - */ | ||
| - FileEditorTab newEditor() { | ||
| - final FileEditorTab tab = createFileEditor( null ); | ||
| - | ||
| - getTabs().add( tab ); | ||
| - getSelectionModel().select( tab ); | ||
| - return tab; | ||
| - } | ||
| - | ||
| - List<FileEditorTab> openFileDialog() { | ||
| - final FileChooser dialog | ||
| - = createFileChooser( get( "Dialog.file.choose.open.title" ) ); | ||
| - final List<File> files = dialog.showOpenMultipleDialog( getWindow() ); | ||
| - | ||
| - return (files != null && !files.isEmpty()) | ||
| - ? openFiles( files ) | ||
| - : new ArrayList<>(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Opens the files into new editors, unless one of those files was a | ||
| - * definition file. The definition file is loaded into the definition pane, | ||
| - * but only the first one selected (multiple definition files will result in a | ||
| - * warning). | ||
| - * | ||
| - * @param files The list of non-definition files that the were requested to | ||
| - * open. | ||
| - * | ||
| - * @return A list of files that can be opened in text editors. | ||
| - */ | ||
| - private List<FileEditorTab> openFiles( final List<File> files ) { | ||
| - final List<FileEditorTab> openedEditors = new ArrayList<>(); | ||
| - | ||
| - final FileTypePredicate predicate | ||
| - = new FileTypePredicate( createExtensionFilter( "definition" ).getExtensions() ); | ||
| - | ||
| - // The user might have opened muliple definitions files. These will | ||
| - // be discarded from the text editable files. | ||
| - final List<File> definitions | ||
| - = files.stream().filter( predicate ).collect( Collectors.toList() ); | ||
| - | ||
| - // Create a modifiable list to remove any definition files that were | ||
| - // opened. | ||
| - final List<File> editors = new ArrayList<>( files ); | ||
| - editors.removeAll( definitions ); | ||
| - | ||
| - // If there are any editor-friendly files opened (e.g,. Markdown, XML), then | ||
| - // open them up in new tabs. | ||
| - if( editors.size() > 0 ) { | ||
| - saveLastDirectory( editors.get( 0 ) ); | ||
| - openedEditors.addAll( openEditors( editors, 0 ) ); | ||
| - } | ||
| - | ||
| - if( definitions.size() > 0 ) { | ||
| - openDefinition( definitions.get( 0 ) ); | ||
| - } | ||
| - | ||
| - return openedEditors; | ||
| - } | ||
| - | ||
| - private List<FileEditorTab> openEditors( final List<File> files, final int activeIndex ) { | ||
| - final int fileTally = files.size(); | ||
| - final List<FileEditorTab> editors = new ArrayList<>( fileTally ); | ||
| - final List<Tab> tabs = getTabs(); | ||
| - | ||
| - // Close single unmodified "Untitled" tab. | ||
| - if( tabs.size() == 1 ) { | ||
| - final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 ).getUserData()); | ||
| - | ||
| - if( fileEditor.getPath() == null && !fileEditor.isModified() ) { | ||
| - closeEditor( fileEditor, false ); | ||
| - } | ||
| - } | ||
| - | ||
| - for( int i = 0; i < fileTally; i++ ) { | ||
| - final Path path = files.get( i ).toPath(); | ||
| - | ||
| - // Check whether file is already opened. | ||
| - FileEditorTab fileEditor = findEditor( path ); | ||
| - | ||
| - if( fileEditor == null ) { | ||
| - fileEditor = createFileEditor( path ); | ||
| - getTabs().add( fileEditor ); | ||
| - editors.add( fileEditor ); | ||
| - } | ||
| - | ||
| - // Select first file. | ||
| - if( i == activeIndex ) { | ||
| - getSelectionModel().select( fileEditor ); | ||
| - } | ||
| - } | ||
| - | ||
| - return editors; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Called when the user has opened a definition file (using the file open | ||
| - * dialog box). This will replace the current set of definitions for the | ||
| - * active tab. | ||
| - * | ||
| - * @param definition The file to open. | ||
| - */ | ||
| - private void openDefinition( final File definition ) { | ||
| - System.out.println( "open definition file: " + definition.toString() ); | ||
| - } | ||
| - | ||
| - boolean saveEditor( final FileEditorTab fileEditor ) { | ||
| - if( fileEditor == null || !fileEditor.isModified() ) { | ||
| - return true; | ||
| - } | ||
| - | ||
| - if( fileEditor.getPath() == null ) { | ||
| - getSelectionModel().select( fileEditor ); | ||
| - | ||
| - final FileChooser fileChooser = createFileChooser( Messages.get( "Dialog.file.choose.save.title" ) ); | ||
| - final File file = fileChooser.showSaveDialog( getWindow() ); | ||
| - if( file == null ) { | ||
| - return false; | ||
| - } | ||
| - | ||
| - saveLastDirectory( file ); | ||
| - fileEditor.setPath( file.toPath() ); | ||
| - } | ||
| - | ||
| - return fileEditor.save(); | ||
| - } | ||
| - | ||
| - boolean saveAllEditors() { | ||
| - boolean success = true; | ||
| - | ||
| - for( FileEditorTab fileEditor : getAllEditors() ) { | ||
| - if( !saveEditor( fileEditor ) ) { | ||
| - success = false; | ||
| - } | ||
| - } | ||
| - | ||
| - return success; | ||
| - } | ||
| - | ||
| - boolean canCloseEditor( final FileEditorTab tab ) { | ||
| - if( !tab.isModified() ) { | ||
| - return true; | ||
| - } | ||
| - | ||
| - final AlertMessage message = getAlertService().createAlertMessage( | ||
| - Messages.get( "Alert.file.close.title" ), | ||
| - Messages.get( "Alert.file.close.text" ), | ||
| - tab.getText() | ||
| - ); | ||
| - | ||
| - final Alert alert = getAlertService().createAlertConfirmation( message ); | ||
| - final ButtonType response = alert.showAndWait().get(); | ||
| - | ||
| - return response == YES ? saveEditor( tab ) : response == NO; | ||
| - } | ||
| - | ||
| - private AlertService getAlertService() { | ||
| - return this.alertService; | ||
| - } | ||
| - | ||
| - boolean closeEditor( FileEditorTab fileEditor, boolean save ) { | ||
| - if( fileEditor == null ) { | ||
| - return true; | ||
| - } | ||
| - | ||
| - final Tab tab = fileEditor; | ||
| - | ||
| - if( save ) { | ||
| - Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT ); | ||
| - Event.fireEvent( tab, event ); | ||
| - | ||
| - if( event.isConsumed() ) { | ||
| - return false; | ||
| - } | ||
| - } | ||
| - | ||
| - getTabs().remove( tab ); | ||
| - | ||
| - if( tab.getOnClosed() != null ) { | ||
| - Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) ); | ||
| - } | ||
| - | ||
| - return true; | ||
| - } | ||
| - | ||
| - boolean closeAllEditors() { | ||
| - final FileEditorTab[] allEditors = getAllEditors(); | ||
| - final FileEditorTab activeEditor = getActiveFileEditor(); | ||
| - | ||
| - // try to save active tab first because in case the user decides to cancel, | ||
| - // then it stays active | ||
| - if( activeEditor != null && !canCloseEditor( activeEditor ) ) { | ||
| - return false; | ||
| - } | ||
| - | ||
| - // This should be called any time a tab changes. | ||
| - persistPreferences(); | ||
| - | ||
| - // save modified tabs | ||
| - for( int i = 0; i < allEditors.length; i++ ) { | ||
| - final FileEditorTab fileEditor = allEditors[ i ]; | ||
| - | ||
| - if( fileEditor == activeEditor ) { | ||
| - continue; | ||
| - } | ||
| - | ||
| - if( fileEditor.isModified() ) { | ||
| - // activate the modified tab to make its modified content visible to the user | ||
| - getSelectionModel().select( i ); | ||
| - | ||
| - if( !canCloseEditor( fileEditor ) ) { | ||
| - return false; | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - // Close all tabs. | ||
| - for( final FileEditorTab fileEditor : allEditors ) { | ||
| - if( !closeEditor( fileEditor, false ) ) { | ||
| - return false; | ||
| - } | ||
| - } | ||
| - | ||
| - return getTabs().isEmpty(); | ||
| - } | ||
| - | ||
| - private FileEditorTab[] getAllEditors() { | ||
| - final ObservableList<Tab> tabs = getTabs(); | ||
| - final FileEditorTab[] allEditors = new FileEditorTab[ tabs.size() ]; | ||
| - final int length = tabs.size(); | ||
| - | ||
| - for( int i = 0; i < length; i++ ) { | ||
| - allEditors[ i ] = (FileEditorTab)tabs.get( i ).getUserData(); | ||
| - } | ||
| - | ||
| - return allEditors; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the file editor tab that has the given path. | ||
| - * | ||
| - * @return null No file editor tab for the given path was found. | ||
| - */ | ||
| - private FileEditorTab findEditor( final Path path ) { | ||
| - for( final Tab tab : getTabs() ) { | ||
| - final FileEditorTab fileEditor = (FileEditorTab)tab; | ||
| - | ||
| - System.out.println( "path = " + path ); | ||
| - System.out.println( "fileEditor = " + fileEditor.isPath( path ) ); | ||
| - | ||
| - if( fileEditor.isPath( path ) ) { | ||
| - return fileEditor; | ||
| - } | ||
| - } | ||
| - | ||
| - return null; | ||
| - } | ||
| - | ||
| - private FileChooser createFileChooser( String title ) { | ||
| - final FileChooser fileChooser = new FileChooser(); | ||
| - | ||
| - fileChooser.setTitle( title ); | ||
| - fileChooser.getExtensionFilters().addAll( | ||
| - createExtensionFilters() ); | ||
| - | ||
| - final String lastDirectory = getState().get( "lastDirectory", null ); | ||
| - File file = new File( (lastDirectory != null) ? lastDirectory : "." ); | ||
| - | ||
| - if( !file.isDirectory() ) { | ||
| - file = new File( "." ); | ||
| - } | ||
| - | ||
| - fileChooser.setInitialDirectory( file ); | ||
| - return fileChooser; | ||
| - } | ||
| - | ||
| - private List<ExtensionFilter> createExtensionFilters() { | ||
| - final List<ExtensionFilter> list = new ArrayList<>(); | ||
| - | ||
| - // TODO: Return a list of all properties that match the filter prefix. | ||
| - // This will allow dynamic filters to be added and removed just by | ||
| - // updating the properties file. | ||
| - list.add( createExtensionFilter( "markdown" ) ); | ||
| - list.add( createExtensionFilter( "definition" ) ); | ||
| - list.add( createExtensionFilter( "xml" ) ); | ||
| - list.add( createExtensionFilter( "all" ) ); | ||
| - return list; | ||
| - } | ||
| - | ||
| - private ExtensionFilter createExtensionFilter( final String filetype ) { | ||
| - final String tKey = String.format( "%s.title.%s", FILTER_PREFIX, filetype ); | ||
| - final String eKey = String.format( "%s.ext.%s", FILTER_PREFIX, filetype ); | ||
| - | ||
| - return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) ); | ||
| - } | ||
| - | ||
| - private List<String> getExtensions( final String key ) { | ||
| - return getStringSettingList( key ); | ||
| - } | ||
| - | ||
| - private List<String> getStringSettingList( String key ) { | ||
| - return getStringSettingList( key, null ); | ||
| - } | ||
| - | ||
| - private List<String> getStringSettingList( String key, List<String> values ) { | ||
| - return getSettings().getStringSettingList( key, values ); | ||
| - } | ||
| - | ||
| - private void saveLastDirectory( final File file ) { | ||
| - getState().put( "lastDirectory", file.getParent() ); | ||
| - } | ||
| - | ||
| - public void restorePreferences() { | ||
| - int activeIndex = 0; | ||
| - | ||
| - final Preferences preferences = getState(); | ||
| - final String[] fileNames = Utils.getPrefsStrings( preferences, "file" ); | ||
| - final String activeFileName = preferences.get( "activeFile", null ); | ||
| - | ||
| - final ArrayList<File> files = new ArrayList<>( fileNames.length ); | ||
| - | ||
| - for( final String fileName : fileNames ) { | ||
| - final File file = new File( fileName ); | ||
| - | ||
| - if( file.exists() ) { | ||
| - files.add( file ); | ||
| - | ||
| - if( fileName.equals( activeFileName ) ) { | ||
| - activeIndex = files.size() - 1; | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - if( files.isEmpty() ) { | ||
| - newEditor(); | ||
| - return; | ||
| - } | ||
| - | ||
| - openEditors( files, activeIndex ); | ||
| - } | ||
| - | ||
| - public void persistPreferences() { | ||
| - final ObservableList<Tab> allEditors = getTabs(); | ||
| - final List<String> fileNames = new ArrayList<>( allEditors.size() ); | ||
| - | ||
| - for( final Tab tab : allEditors ) { | ||
| - final FileEditorTab fileEditor = (FileEditorTab)tab; | ||
| - | ||
| - if( fileEditor.getPath() != null ) { | ||
| - fileNames.add( fileEditor.getPath().toString() ); | ||
| - } | ||
| - } | ||
| - | ||
| - final Preferences preferences = getState(); | ||
| - Utils.putPrefsStrings( preferences, "file", fileNames.toArray( new String[ fileNames.size() ] ) ); | ||
| - | ||
| - final FileEditorTab activeEditor = getActiveFileEditor(); | ||
| - | ||
| - if( activeEditor != null && activeEditor.getPath() != null ) { | ||
| - preferences.put( "activeFile", activeEditor.getPath().toString() ); | ||
| - } else { | ||
| - preferences.remove( "activeFile" ); | ||
| - } | ||
| - } | ||
| - | ||
| - private Settings getSettings() { | ||
| - return this.settings; | ||
| - } | ||
| - | ||
| - protected Options getOptions() { | ||
| - return this.options; | ||
| - } | ||
| - | ||
| - private Window getWindow() { | ||
| - return getScene().getWindow(); | ||
| - } | ||
| - | ||
| +public final class FileEditorTabPane extends TabPane { | ||
| + | ||
| + private final static String FILTER_PREFIX = "Dialog.file.choose.filter"; | ||
| + | ||
| + private final Options options = Services.load( Options.class ); | ||
| + private final Settings settings = Services.load( Settings.class ); | ||
| + private final AlertService alertService = Services.load( AlertService.class ); | ||
| + | ||
| + private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>(); | ||
| + private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper(); | ||
| + | ||
| + public FileEditorTabPane() { | ||
| + final ObservableList<Tab> tabs = getTabs(); | ||
| + | ||
| + setFocusTraversable( false ); | ||
| + setTabClosingPolicy( TabClosingPolicy.ALL_TABS ); | ||
| + | ||
| + addTabChangeListener( (ObservableValue<? extends Tab> tabPane, | ||
| + final Tab oldTab, final Tab newTab) -> { | ||
| + if( newTab != null ) { | ||
| + activeFileEditor.set( (FileEditorTab)newTab.getUserData() ); | ||
| + } | ||
| + } ); | ||
| + | ||
| + final ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> { | ||
| + for( final Tab tab : tabs ) { | ||
| + if( ((FileEditorTab)tab.getUserData()).isModified() ) { | ||
| + this.anyFileEditorModified.set( true ); | ||
| + break; | ||
| + } | ||
| + } | ||
| + }; | ||
| + | ||
| + tabs.addListener( (ListChangeListener<Tab>)change -> { | ||
| + while( change.next() ) { | ||
| + if( change.wasAdded() ) { | ||
| + change.getAddedSubList().stream().forEach( (tab) -> { | ||
| + ((FileEditorTab)tab.getUserData()).modifiedProperty().addListener( modifiedListener ); | ||
| + } ); | ||
| + } else if( change.wasRemoved() ) { | ||
| + change.getRemoved().stream().forEach( (tab) -> { | ||
| + ((FileEditorTab)tab.getUserData()).modifiedProperty().removeListener( modifiedListener ); | ||
| + } ); | ||
| + } | ||
| + } | ||
| + | ||
| + // Changes in the tabs may also change anyFileEditorModified property | ||
| + // (e.g. closed modified file) | ||
| + modifiedListener.changed( null, null, null ); | ||
| + } ); | ||
| + } | ||
| + | ||
| + public <T extends Event, U extends T> void addEventListener( | ||
| + final EventPattern<? super T, ? extends U> event, | ||
| + final Consumer<? super U> consumer ) { | ||
| + getActiveFileEditor().addEventListener( event, consumer ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Delegates to the active file editor pane, and, ultimately, to its text | ||
| + * area. | ||
| + * | ||
| + * @param map The map of methods to events. | ||
| + */ | ||
| + public void addEventListener( final InputMap<InputEvent> map ) { | ||
| + getActiveFileEditor().addEventListener( map ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Remove a keyboard event listener from the active file editor. | ||
| + * | ||
| + * @param map The keyboard events to remove. | ||
| + */ | ||
| + public void removeEventListener( final InputMap<InputEvent> map ) { | ||
| + getActiveFileEditor().removeEventListener( map ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Allows observers to be notified when the current file editor tab changes. | ||
| + * | ||
| + * @param listener The listener to notify of tab change events. | ||
| + */ | ||
| + public void addTabChangeListener( final ChangeListener<Tab> listener ) { | ||
| + // Observe the tab so that when a new tab is opened or selected, | ||
| + // a notification is kicked off. | ||
| + getSelectionModel().selectedItemProperty().addListener( listener ); | ||
| + } | ||
| + | ||
| + Node getNode() { | ||
| + return this; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Allows clients to manipulate the editor content directly. | ||
| + * | ||
| + * @return The text area for the active file editor. | ||
| + */ | ||
| + public StyledTextArea getEditor() { | ||
| + return getActiveFileEditor().getEditorPane().getEditor(); | ||
| + } | ||
| + | ||
| + public FileEditorTab getActiveFileEditor() { | ||
| + return this.activeFileEditor.get(); | ||
| + } | ||
| + | ||
| + ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() { | ||
| + return this.activeFileEditor.getReadOnlyProperty(); | ||
| + } | ||
| + | ||
| + ReadOnlyBooleanProperty anyFileEditorModifiedProperty() { | ||
| + return this.anyFileEditorModified.getReadOnlyProperty(); | ||
| + } | ||
| + | ||
| + private FileEditorTab createFileEditor( final Path path ) { | ||
| + final FileEditorTab tab = new FileEditorTab( path ); | ||
| + | ||
| + tab.setOnCloseRequest( e -> { | ||
| + if( !canCloseEditor( tab ) ) { | ||
| + e.consume(); | ||
| + } | ||
| + } ); | ||
| + | ||
| + return tab; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Called when the user selects New from the File menu. | ||
| + * | ||
| + * @return The newly added tab. | ||
| + */ | ||
| + FileEditorTab newEditor() { | ||
| + final FileEditorTab tab = createFileEditor( null ); | ||
| + | ||
| + getTabs().add( tab ); | ||
| + getSelectionModel().select( tab ); | ||
| + return tab; | ||
| + } | ||
| + | ||
| + List<FileEditorTab> openFileDialog() { | ||
| + final FileChooser dialog | ||
| + = createFileChooser( get( "Dialog.file.choose.open.title" ) ); | ||
| + final List<File> files = dialog.showOpenMultipleDialog( getWindow() ); | ||
| + | ||
| + return (files != null && !files.isEmpty()) | ||
| + ? openFiles( files ) | ||
| + : new ArrayList<>(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Opens the files into new editors, unless one of those files was a | ||
| + * definition file. The definition file is loaded into the definition pane, | ||
| + * but only the first one selected (multiple definition files will result in a | ||
| + * warning). | ||
| + * | ||
| + * @param files The list of non-definition files that the were requested to | ||
| + * open. | ||
| + * | ||
| + * @return A list of files that can be opened in text editors. | ||
| + */ | ||
| + private List<FileEditorTab> openFiles( final List<File> files ) { | ||
| + final List<FileEditorTab> openedEditors = new ArrayList<>(); | ||
| + | ||
| + final FileTypePredicate predicate | ||
| + = new FileTypePredicate( createExtensionFilter( "definition" ).getExtensions() ); | ||
| + | ||
| + // The user might have opened muliple definitions files. These will | ||
| + // be discarded from the text editable files. | ||
| + final List<File> definitions | ||
| + = files.stream().filter( predicate ).collect( Collectors.toList() ); | ||
| + | ||
| + // Create a modifiable list to remove any definition files that were | ||
| + // opened. | ||
| + final List<File> editors = new ArrayList<>( files ); | ||
| + editors.removeAll( definitions ); | ||
| + | ||
| + // If there are any editor-friendly files opened (e.g,. Markdown, XML), then | ||
| + // open them up in new tabs. | ||
| + if( editors.size() > 0 ) { | ||
| + saveLastDirectory( editors.get( 0 ) ); | ||
| + openedEditors.addAll( openEditors( editors, 0 ) ); | ||
| + } | ||
| + | ||
| + if( definitions.size() > 0 ) { | ||
| + openDefinition( definitions.get( 0 ) ); | ||
| + } | ||
| + | ||
| + return openedEditors; | ||
| + } | ||
| + | ||
| + private List<FileEditorTab> openEditors( final List<File> files, final int activeIndex ) { | ||
| + final int fileTally = files.size(); | ||
| + final List<FileEditorTab> editors = new ArrayList<>( fileTally ); | ||
| + final List<Tab> tabs = getTabs(); | ||
| + | ||
| + // Close single unmodified "Untitled" tab. | ||
| + if( tabs.size() == 1 ) { | ||
| + final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 ).getUserData()); | ||
| + | ||
| + if( fileEditor.getPath() == null && !fileEditor.isModified() ) { | ||
| + closeEditor( fileEditor, false ); | ||
| + } | ||
| + } | ||
| + | ||
| + for( int i = 0; i < fileTally; i++ ) { | ||
| + final Path path = files.get( i ).toPath(); | ||
| + | ||
| + // Check whether file is already opened. | ||
| + FileEditorTab fileEditor = findEditor( path ); | ||
| + | ||
| + if( fileEditor == null ) { | ||
| + fileEditor = createFileEditor( path ); | ||
| + getTabs().add( fileEditor ); | ||
| + editors.add( fileEditor ); | ||
| + } | ||
| + | ||
| + // Select first file. | ||
| + if( i == activeIndex ) { | ||
| + getSelectionModel().select( fileEditor ); | ||
| + } | ||
| + } | ||
| + | ||
| + return editors; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Called when the user has opened a definition file (using the file open | ||
| + * dialog box). This will replace the current set of definitions for the | ||
| + * active tab. | ||
| + * | ||
| + * @param definition The file to open. | ||
| + */ | ||
| + private void openDefinition( final File definition ) { | ||
| + System.out.println( "open definition file: " + definition.toString() ); | ||
| + } | ||
| + | ||
| + boolean saveEditor( final FileEditorTab fileEditor ) { | ||
| + if( fileEditor == null || !fileEditor.isModified() ) { | ||
| + return true; | ||
| + } | ||
| + | ||
| + if( fileEditor.getPath() == null ) { | ||
| + getSelectionModel().select( fileEditor ); | ||
| + | ||
| + final FileChooser fileChooser = createFileChooser( Messages.get( "Dialog.file.choose.save.title" ) ); | ||
| + final File file = fileChooser.showSaveDialog( getWindow() ); | ||
| + if( file == null ) { | ||
| + return false; | ||
| + } | ||
| + | ||
| + saveLastDirectory( file ); | ||
| + fileEditor.setPath( file.toPath() ); | ||
| + } | ||
| + | ||
| + return fileEditor.save(); | ||
| + } | ||
| + | ||
| + boolean saveAllEditors() { | ||
| + boolean success = true; | ||
| + | ||
| + for( FileEditorTab fileEditor : getAllEditors() ) { | ||
| + if( !saveEditor( fileEditor ) ) { | ||
| + success = false; | ||
| + } | ||
| + } | ||
| + | ||
| + return success; | ||
| + } | ||
| + | ||
| + boolean canCloseEditor( final FileEditorTab tab ) { | ||
| + if( !tab.isModified() ) { | ||
| + return true; | ||
| + } | ||
| + | ||
| + final AlertMessage message = getAlertService().createAlertMessage( | ||
| + Messages.get( "Alert.file.close.title" ), | ||
| + Messages.get( "Alert.file.close.text" ), | ||
| + tab.getText() | ||
| + ); | ||
| + | ||
| + final Alert alert = getAlertService().createAlertConfirmation( message ); | ||
| + final ButtonType response = alert.showAndWait().get(); | ||
| + | ||
| + return response == YES ? saveEditor( tab ) : response == NO; | ||
| + } | ||
| + | ||
| + private AlertService getAlertService() { | ||
| + return this.alertService; | ||
| + } | ||
| + | ||
| + boolean closeEditor( FileEditorTab fileEditor, boolean save ) { | ||
| + if( fileEditor == null ) { | ||
| + return true; | ||
| + } | ||
| + | ||
| + final Tab tab = fileEditor; | ||
| + | ||
| + if( save ) { | ||
| + Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT ); | ||
| + Event.fireEvent( tab, event ); | ||
| + | ||
| + if( event.isConsumed() ) { | ||
| + return false; | ||
| + } | ||
| + } | ||
| + | ||
| + getTabs().remove( tab ); | ||
| + | ||
| + if( tab.getOnClosed() != null ) { | ||
| + Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) ); | ||
| + } | ||
| + | ||
| + return true; | ||
| + } | ||
| + | ||
| + boolean closeAllEditors() { | ||
| + final FileEditorTab[] allEditors = getAllEditors(); | ||
| + final FileEditorTab activeEditor = getActiveFileEditor(); | ||
| + | ||
| + // try to save active tab first because in case the user decides to cancel, | ||
| + // then it stays active | ||
| + if( activeEditor != null && !canCloseEditor( activeEditor ) ) { | ||
| + return false; | ||
| + } | ||
| + | ||
| + // This should be called any time a tab changes. | ||
| + persistPreferences(); | ||
| + | ||
| + // save modified tabs | ||
| + for( int i = 0; i < allEditors.length; i++ ) { | ||
| + final FileEditorTab fileEditor = allEditors[ i ]; | ||
| + | ||
| + if( fileEditor == activeEditor ) { | ||
| + continue; | ||
| + } | ||
| + | ||
| + if( fileEditor.isModified() ) { | ||
| + // activate the modified tab to make its modified content visible to the user | ||
| + getSelectionModel().select( i ); | ||
| + | ||
| + if( !canCloseEditor( fileEditor ) ) { | ||
| + return false; | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + // Close all tabs. | ||
| + for( final FileEditorTab fileEditor : allEditors ) { | ||
| + if( !closeEditor( fileEditor, false ) ) { | ||
| + return false; | ||
| + } | ||
| + } | ||
| + | ||
| + return getTabs().isEmpty(); | ||
| + } | ||
| + | ||
| + private FileEditorTab[] getAllEditors() { | ||
| + final ObservableList<Tab> tabs = getTabs(); | ||
| + final int length = tabs.size(); | ||
| + final FileEditorTab[] allEditors = new FileEditorTab[ length ]; | ||
| + | ||
| + for( int i = 0; i < length; i++ ) { | ||
| + allEditors[ i ] = (FileEditorTab)tabs.get( i ).getUserData(); | ||
| + } | ||
| + | ||
| + return allEditors; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the file editor tab that has the given path. | ||
| + * | ||
| + * @return null No file editor tab for the given path was found. | ||
| + */ | ||
| + private FileEditorTab findEditor( final Path path ) { | ||
| + for( final Tab tab : getTabs() ) { | ||
| + final FileEditorTab fileEditor = (FileEditorTab)tab; | ||
| + | ||
| + if( fileEditor.isPath( path ) ) { | ||
| + return fileEditor; | ||
| + } | ||
| + } | ||
| + | ||
| + return null; | ||
| + } | ||
| + | ||
| + private FileChooser createFileChooser( String title ) { | ||
| + final FileChooser fileChooser = new FileChooser(); | ||
| + | ||
| + fileChooser.setTitle( title ); | ||
| + fileChooser.getExtensionFilters().addAll( | ||
| + createExtensionFilters() ); | ||
| + | ||
| + final String lastDirectory = getState().get( "lastDirectory", null ); | ||
| + File file = new File( (lastDirectory != null) ? lastDirectory : "." ); | ||
| + | ||
| + if( !file.isDirectory() ) { | ||
| + file = new File( "." ); | ||
| + } | ||
| + | ||
| + fileChooser.setInitialDirectory( file ); | ||
| + return fileChooser; | ||
| + } | ||
| + | ||
| + private List<ExtensionFilter> createExtensionFilters() { | ||
| + final List<ExtensionFilter> list = new ArrayList<>(); | ||
| + | ||
| + // TODO: Return a list of all properties that match the filter prefix. | ||
| + // This will allow dynamic filters to be added and removed just by | ||
| + // updating the properties file. | ||
| + list.add( createExtensionFilter( "markdown" ) ); | ||
| + list.add( createExtensionFilter( "definition" ) ); | ||
| + list.add( createExtensionFilter( "xml" ) ); | ||
| + list.add( createExtensionFilter( "all" ) ); | ||
| + return list; | ||
| + } | ||
| + | ||
| + private ExtensionFilter createExtensionFilter( final String filetype ) { | ||
| + final String tKey = String.format( "%s.title.%s", FILTER_PREFIX, filetype ); | ||
| + final String eKey = String.format( "%s.ext.%s", FILTER_PREFIX, filetype ); | ||
| + | ||
| + return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) ); | ||
| + } | ||
| + | ||
| + private List<String> getExtensions( final String key ) { | ||
| + return getStringSettingList( key ); | ||
| + } | ||
| + | ||
| + private List<String> getStringSettingList( String key ) { | ||
| + return getStringSettingList( key, null ); | ||
| + } | ||
| + | ||
| + private List<String> getStringSettingList( String key, List<String> values ) { | ||
| + return getSettings().getStringSettingList( key, values ); | ||
| + } | ||
| + | ||
| + private void saveLastDirectory( final File file ) { | ||
| + getState().put( "lastDirectory", file.getParent() ); | ||
| + } | ||
| + | ||
| + public void restorePreferences() { | ||
| + int activeIndex = 0; | ||
| + | ||
| + final Preferences preferences = getState(); | ||
| + final String[] fileNames = Utils.getPrefsStrings( preferences, "file" ); | ||
| + final String activeFileName = preferences.get( "activeFile", null ); | ||
| + | ||
| + final ArrayList<File> files = new ArrayList<>( fileNames.length ); | ||
| + | ||
| + for( final String fileName : fileNames ) { | ||
| + final File file = new File( fileName ); | ||
| + | ||
| + if( file.exists() ) { | ||
| + files.add( file ); | ||
| + | ||
| + if( fileName.equals( activeFileName ) ) { | ||
| + activeIndex = files.size() - 1; | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + if( files.isEmpty() ) { | ||
| + newEditor(); | ||
| + return; | ||
| + } | ||
| + | ||
| + openEditors( files, activeIndex ); | ||
| + } | ||
| + | ||
| + public void persistPreferences() { | ||
| + final ObservableList<Tab> allEditors = getTabs(); | ||
| + final List<String> fileNames = new ArrayList<>( allEditors.size() ); | ||
| + | ||
| + for( final Tab tab : allEditors ) { | ||
| + final FileEditorTab fileEditor = (FileEditorTab)tab; | ||
| + | ||
| + if( fileEditor.getPath() != null ) { | ||
| + fileNames.add( fileEditor.getPath().toString() ); | ||
| + } | ||
| + } | ||
| + | ||
| + final Preferences preferences = getState(); | ||
| + Utils.putPrefsStrings( preferences, "file", fileNames.toArray( new String[ fileNames.size() ] ) ); | ||
| + | ||
| + final FileEditorTab activeEditor = getActiveFileEditor(); | ||
| + | ||
| + if( activeEditor != null && activeEditor.getPath() != null ) { | ||
| + preferences.put( "activeFile", activeEditor.getPath().toString() ); | ||
| + } else { | ||
| + preferences.remove( "activeFile" ); | ||
| + } | ||
| + } | ||
| + | ||
| + private Settings getSettings() { | ||
| + return this.settings; | ||
| + } | ||
| + | ||
| + protected Options getOptions() { | ||
| + return this.options; | ||
| + } | ||
| + | ||
| + private Window getWindow() { | ||
| + return getScene().getWindow(); | ||
| + } | ||
| + | ||
| protected Preferences getState() { | ||
| return getOptions().getState(); |
| import static com.scrivenvar.util.StageState.K_PANE_SPLIT_DEFINITION; | ||
| import static com.scrivenvar.util.StageState.K_PANE_SPLIT_EDITOR; | ||
| -import com.scrivenvar.yaml.YamlParser; | ||
| -import com.scrivenvar.yaml.YamlTreeAdapter; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO; | ||
| -import java.io.IOException; | ||
| -import java.io.InputStream; | ||
| -import java.util.Map; | ||
| -import java.util.function.Function; | ||
| -import java.util.prefs.Preferences; | ||
| -import javafx.beans.binding.Bindings; | ||
| -import javafx.beans.binding.BooleanBinding; | ||
| -import javafx.beans.property.BooleanProperty; | ||
| -import javafx.beans.property.SimpleBooleanProperty; | ||
| -import javafx.beans.value.ObservableBooleanValue; | ||
| -import javafx.beans.value.ObservableValue; | ||
| -import javafx.collections.ListChangeListener.Change; | ||
| -import javafx.collections.ObservableList; | ||
| -import javafx.event.Event; | ||
| -import javafx.scene.Node; | ||
| -import javafx.scene.Scene; | ||
| -import javafx.scene.control.Alert; | ||
| -import javafx.scene.control.Alert.AlertType; | ||
| -import javafx.scene.control.Menu; | ||
| -import javafx.scene.control.MenuBar; | ||
| -import javafx.scene.control.SplitPane; | ||
| -import javafx.scene.control.Tab; | ||
| -import javafx.scene.control.ToolBar; | ||
| -import javafx.scene.control.TreeView; | ||
| -import javafx.scene.image.Image; | ||
| -import javafx.scene.image.ImageView; | ||
| -import static javafx.scene.input.KeyCode.ESCAPE; | ||
| -import javafx.scene.input.KeyEvent; | ||
| -import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED; | ||
| -import static javafx.scene.input.KeyEvent.KEY_PRESSED; | ||
| -import javafx.scene.layout.BorderPane; | ||
| -import javafx.scene.layout.VBox; | ||
| -import javafx.stage.Window; | ||
| -import javafx.stage.WindowEvent; | ||
| -import org.fxmisc.richtext.StyleClassedTextArea; | ||
| -import static com.scrivenvar.Messages.get; | ||
| -import static com.scrivenvar.Messages.get; | ||
| -import static com.scrivenvar.Messages.get; | ||
| -import static com.scrivenvar.Messages.get; | ||
| -import static com.scrivenvar.Messages.get; | ||
| -import static com.scrivenvar.Messages.get; | ||
| -import static com.scrivenvar.Messages.get; | ||
| - | ||
| -/** | ||
| - * Main window containing a tab pane in the center for file editors. | ||
| - * | ||
| - * @author Karl Tauber and White Magic Software, Ltd. | ||
| - */ | ||
| -public class MainWindow { | ||
| - | ||
| - private final Options options = Services.load( Options.class ); | ||
| - | ||
| - private Scene scene; | ||
| - | ||
| - private TreeView<String> treeView; | ||
| - private FileEditorTabPane fileEditorPane; | ||
| - private DefinitionPane definitionPane; | ||
| - | ||
| - private VariableNameInjector variableNameInjector; | ||
| - | ||
| - private YamlTreeAdapter yamlTreeAdapter; | ||
| - private YamlParser yamlParser; | ||
| - | ||
| - private MenuBar menuBar; | ||
| - | ||
| - public MainWindow() { | ||
| - initLayout(); | ||
| - initVariableNameInjector(); | ||
| - } | ||
| - | ||
| - private void initLayout() { | ||
| - final SplitPane splitPane = new SplitPane( | ||
| - getDefinitionPane().getNode(), | ||
| - getFileEditorPane().getNode() ); | ||
| - | ||
| - splitPane.setDividerPositions( | ||
| - getFloat( K_PANE_SPLIT_DEFINITION, .05f ), | ||
| - getFloat( K_PANE_SPLIT_EDITOR, .95f ) ); | ||
| - | ||
| - // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html | ||
| - final BorderPane borderPane = new BorderPane(); | ||
| - borderPane.setPrefSize( 1024, 800 ); | ||
| - borderPane.setTop( createMenuBar() ); | ||
| - borderPane.setCenter( splitPane ); | ||
| - | ||
| - final Scene appScene = new Scene( borderPane ); | ||
| - setScene( appScene ); | ||
| - appScene.getStylesheets().add( Constants.STYLESHEET_PREVIEW ); | ||
| - appScene.windowProperty().addListener( | ||
| - (observable, oldWindow, newWindow) -> { | ||
| - newWindow.setOnCloseRequest( e -> { | ||
| - if( !getFileEditorPane().closeAllEditors() ) { | ||
| - e.consume(); | ||
| - } | ||
| - } ); | ||
| - | ||
| - // Workaround JavaFX bug: deselect menubar if window loses focus. | ||
| - newWindow.focusedProperty().addListener( | ||
| - (obs, oldFocused, newFocused) -> { | ||
| - if( !newFocused ) { | ||
| - // Send an ESC key event to the menubar | ||
| - this.menuBar.fireEvent( | ||
| - new KeyEvent( | ||
| - KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE, | ||
| - false, false, false, false ) ); | ||
| - } | ||
| - } ); | ||
| - } ); | ||
| - } | ||
| - | ||
| - private void initVariableNameInjector() { | ||
| - setVariableNameInjector( new VariableNameInjector( | ||
| - getFileEditorPane(), | ||
| - getDefinitionPane() ) | ||
| - ); | ||
| - } | ||
| - | ||
| - private Window getWindow() { | ||
| - return getScene().getWindow(); | ||
| - } | ||
| - | ||
| - public Scene getScene() { | ||
| - return this.scene; | ||
| - } | ||
| - | ||
| - private void setScene( Scene scene ) { | ||
| - this.scene = scene; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Creates a boolean property that is bound to another boolean value of the | ||
| - * active editor. | ||
| - */ | ||
| - private BooleanProperty createActiveBooleanProperty( | ||
| - final Function<FileEditorTab, ObservableBooleanValue> func ) { | ||
| - | ||
| - final BooleanProperty b = new SimpleBooleanProperty(); | ||
| - final FileEditorTab tab = getActiveFileEditor(); | ||
| - | ||
| - if( tab != null ) { | ||
| - b.bind( func.apply( tab ) ); | ||
| - } | ||
| - | ||
| - getFileEditorPane().activeFileEditorProperty().addListener( | ||
| - (observable, oldFileEditor, newFileEditor) -> { | ||
| - b.unbind(); | ||
| - | ||
| - if( newFileEditor != null ) { | ||
| - b.bind( func.apply( newFileEditor ) ); | ||
| - } else { | ||
| - b.set( false ); | ||
| - } | ||
| - } ); | ||
| - | ||
| - return b; | ||
| - } | ||
| - | ||
| - //---- File actions ------------------------------------------------------- | ||
| - private void fileNew() { | ||
| - getFileEditorPane().newEditor(); | ||
| - } | ||
| - | ||
| - private void fileOpen() { | ||
| - getFileEditorPane().openFileDialog(); | ||
| - } | ||
| - | ||
| - private void fileClose() { | ||
| - getFileEditorPane().closeEditor( getActiveFileEditor(), true ); | ||
| - } | ||
| - | ||
| - private void fileCloseAll() { | ||
| - getFileEditorPane().closeAllEditors(); | ||
| - } | ||
| - | ||
| - private void fileSave() { | ||
| - getFileEditorPane().saveEditor( getActiveFileEditor() ); | ||
| - } | ||
| - | ||
| - private void fileSaveAll() { | ||
| - getFileEditorPane().saveAllEditors(); | ||
| - } | ||
| - | ||
| - private void fileExit() { | ||
| - final Window window = getWindow(); | ||
| - Event.fireEvent( window, | ||
| - new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) ); | ||
| - } | ||
| - | ||
| - //---- Help actions ------------------------------------------------------- | ||
| - private void helpAbout() { | ||
| - Alert alert = new Alert( AlertType.INFORMATION ); | ||
| - alert.setTitle( Messages.get( "Dialog.about.title" ) ); | ||
| - alert.setHeaderText( Messages.get( "Dialog.about.header" ) ); | ||
| - alert.setContentText( Messages.get( "Dialog.about.content" ) ); | ||
| - alert.setGraphic( new ImageView( new Image( LOGO_32 ) ) ); | ||
| - alert.initOwner( getWindow() ); | ||
| - | ||
| - alert.showAndWait(); | ||
| - } | ||
| - | ||
| - private FileEditorTabPane getFileEditorPane() { | ||
| - if( this.fileEditorPane == null ) { | ||
| - this.fileEditorPane = createFileEditorPane(); | ||
| - } | ||
| - | ||
| - return this.fileEditorPane; | ||
| - } | ||
| - | ||
| - private FileEditorTabPane createFileEditorPane() { | ||
| - // Create an editor pane to hold file editor tabs. | ||
| - final FileEditorTabPane editorPane = new FileEditorTabPane(); | ||
| - | ||
| - // Make sure the text processor kicks off when new files are opened. | ||
| - final ObservableList<Tab> tabs = editorPane.getTabs(); | ||
| - | ||
| - tabs.addListener( (Change<? extends Tab> change) -> { | ||
| - while( change.next() ) { | ||
| - if( change.wasAdded() ) { | ||
| - // Multiple tabs can be added simultaneously. | ||
| - for( final Tab tab : change.getAddedSubList() ) { | ||
| - addListener( (FileEditorTab)tab ); | ||
| - } | ||
| - } | ||
| - } | ||
| - } ); | ||
| - | ||
| - // After the processors are in place, restorePreferences the previously closed | ||
| - // tabs. Adding them will trigger the change event, above. | ||
| - editorPane.restorePreferences(); | ||
| - | ||
| - return editorPane; | ||
| - } | ||
| - | ||
| - private MarkdownEditorPane getActiveEditor() { | ||
| - return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane()); | ||
| - } | ||
| - | ||
| - private FileEditorTab getActiveFileEditor() { | ||
| - return getFileEditorPane().getActiveFileEditor(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Listens for changes to tabs and their text editors. | ||
| - * | ||
| - * @see https://github.com/DaveJarvis/scrivenvar/issues/17 | ||
| - * @see https://github.com/DaveJarvis/scrivenvar/issues/18 | ||
| - * | ||
| - * @param tab The file editor tab that contains a text editor. | ||
| - */ | ||
| - private void addListener( FileEditorTab tab ) { | ||
| - final HTMLPreviewPane previewPane = tab.getPreviewPane(); | ||
| - final EditorPane editorPanel = tab.getEditorPane(); | ||
| - final StyleClassedTextArea editor = editorPanel.getEditor(); | ||
| - | ||
| - // TODO: Use a factory based on the filename extension. The default | ||
| - // extension will be for a markdown file (e.g., on file new). | ||
| - final Processor<String> hpp = new HTMLPreviewProcessor( previewPane ); | ||
| - final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp ); | ||
| - final Processor<String> mp = new MarkdownProcessor( mcrp ); | ||
| - final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, editor ); | ||
| - final Processor<String> vnp = new VariableProcessor( mcip, getResolvedMap() ); | ||
| - final TextChangeProcessor tp = new TextChangeProcessor( vnp ); | ||
| - | ||
| - editorPanel.addChangeListener( tp ); | ||
| - editorPanel.addCaretParagraphListener( | ||
| - (final ObservableValue<? extends Integer> observable, | ||
| - final Integer oldValue, final Integer newValue) -> { | ||
| - | ||
| - // Kick off the processing chain at the variable processor when the | ||
| - // cursor changes paragraphs. This might cause some slight duplication | ||
| - // when the Enter key is pressed. | ||
| - vnp.processChain( editor.getText() ); | ||
| - } ); | ||
| - } | ||
| - | ||
| - protected DefinitionPane createDefinitionPane() { | ||
| - return new DefinitionPane( getTreeView() ); | ||
| - } | ||
| - | ||
| - private DefinitionPane getDefinitionPane() { | ||
| - if( this.definitionPane == null ) { | ||
| - this.definitionPane = createDefinitionPane(); | ||
| - } | ||
| - | ||
| - return this.definitionPane; | ||
| - } | ||
| - | ||
| - public MenuBar getMenuBar() { | ||
| - return menuBar; | ||
| - } | ||
| - | ||
| - public void setMenuBar( MenuBar menuBar ) { | ||
| - this.menuBar = menuBar; | ||
| - } | ||
| - | ||
| - public VariableNameInjector getVariableNameInjector() { | ||
| - return this.variableNameInjector; | ||
| - } | ||
| - | ||
| - public void setVariableNameInjector( VariableNameInjector variableNameInjector ) { | ||
| - this.variableNameInjector = variableNameInjector; | ||
| - } | ||
| - | ||
| - private float getFloat( final String key, final float defaultValue ) { | ||
| - return getPreferences().getFloat( key, defaultValue ); | ||
| - } | ||
| - | ||
| - private Preferences getPreferences() { | ||
| - return getOptions().getState(); | ||
| - } | ||
| - | ||
| - private Options getOptions() { | ||
| - return this.options; | ||
| - } | ||
| - | ||
| - private synchronized TreeView<String> getTreeView() throws RuntimeException { | ||
| - if( this.treeView == null ) { | ||
| - try { | ||
| - this.treeView = createTreeView(); | ||
| - } catch( IOException ex ) { | ||
| - | ||
| - // TODO: Pop an error message. | ||
| - throw new RuntimeException( ex ); | ||
| - } | ||
| - } | ||
| - | ||
| - return this.treeView; | ||
| - } | ||
| - | ||
| - private InputStream asStream( final String resource ) { | ||
| - return getClass().getResourceAsStream( resource ); | ||
| - } | ||
| - | ||
| - private TreeView<String> createTreeView() throws IOException { | ||
| - // TODO: Associate variable file with path to current file. | ||
| - return getYamlTreeAdapter().adapt( | ||
| - asStream( "/com/scrivenvar/variables.yaml" ), | ||
| - get( "Pane.defintion.node.root.title" ) | ||
| - ); | ||
| - } | ||
| - | ||
| - private Map<String, String> getResolvedMap() { | ||
| - return getYamlParser().createResolvedMap(); | ||
| - } | ||
| - | ||
| - private YamlTreeAdapter getYamlTreeAdapter() { | ||
| - if( this.yamlTreeAdapter == null ) { | ||
| - setYamlTreeAdapter( new YamlTreeAdapter( getYamlParser() ) ); | ||
| - } | ||
| - | ||
| - return this.yamlTreeAdapter; | ||
| - } | ||
| - | ||
| - private void setYamlTreeAdapter( final YamlTreeAdapter yamlTreeAdapter ) { | ||
| - this.yamlTreeAdapter = yamlTreeAdapter; | ||
| - } | ||
| - | ||
| - private YamlParser getYamlParser() { | ||
| - if( this.yamlParser == null ) { | ||
| - setYamlParser( new YamlParser() ); | ||
| - } | ||
| - | ||
| - return this.yamlParser; | ||
| - } | ||
| - | ||
| - private void setYamlParser( final YamlParser yamlParser ) { | ||
| - this.yamlParser = yamlParser; | ||
| - } | ||
| - | ||
| - private Node createMenuBar() { | ||
| - final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull(); | ||
| - | ||
| - // File actions | ||
| - Action fileNewAction = new Action( Messages.get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() ); | ||
| - Action fileOpenAction = new Action( Messages.get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() ); | ||
| - Action fileCloseAction = new Action( Messages.get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull ); | ||
| - Action fileCloseAllAction = new Action( Messages.get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull ); | ||
| - Action fileSaveAction = new Action( Messages.get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(), | ||
| - createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() ); | ||
| - Action fileSaveAllAction = new Action( Messages.get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(), | ||
| - Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) ); | ||
| - Action fileExitAction = new Action( Messages.get( "Main.menu.file.exit" ), null, null, e -> fileExit() ); | ||
| - | ||
| - // Edit actions | ||
| - Action editUndoAction = new Action( Messages.get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO, | ||
| - e -> getActiveEditor().undo(), | ||
| - createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() ); | ||
| - Action editRedoAction = new Action( Messages.get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT, | ||
| - e -> getActiveEditor().redo(), | ||
| - createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() ); | ||
| - | ||
| - // Insert actions | ||
| - Action insertBoldAction = new Action( Messages.get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD, | ||
| - e -> getActiveEditor().surroundSelection( "**", "**" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertItalicAction = new Action( Messages.get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC, | ||
| - e -> getActiveEditor().surroundSelection( "*", "*" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertStrikethroughAction = new Action( Messages.get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH, | ||
| - e -> getActiveEditor().surroundSelection( "~~", "~~" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertBlockquoteAction = new Action( Messages.get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac | ||
| - e -> getActiveEditor().surroundSelection( "\n\n> ", "" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertCodeAction = new Action( Messages.get( "Main.menu.insert.code" ), "Shortcut+K", CODE, | ||
| - e -> getActiveEditor().surroundSelection( "`", "`" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertFencedCodeBlockAction = new Action( Messages.get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT, | ||
| - e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", Messages.get( "Main.menu.insert.fenced_code_block.prompt" ) ), | ||
| - activeFileEditorIsNull ); | ||
| - | ||
| - Action insertLinkAction = new Action( Messages.get( "Main.menu.insert.link" ), "Shortcut+L", LINK, | ||
| - e -> getActiveEditor().insertLink(), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertImageAction = new Action( Messages.get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT, | ||
| - e -> getActiveEditor().insertImage(), | ||
| - activeFileEditorIsNull ); | ||
| - | ||
| - final Action[] headers = new Action[ 6 ]; | ||
| - | ||
| - // Insert header actions (H1 ... H6) | ||
| - for( int i = 1; i <= 6; i++ ) { | ||
| - final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | ||
| - final String markup = String.format( "\n\n%s ", hashes ); | ||
| - final String text = Messages.get( "Main.menu.insert.header_" + i ); | ||
| - final String accelerator = "Shortcut+" + i; | ||
| - final String prompt = Messages.get( "Main.menu.insert.header_" + i + ".prompt" ); | ||
| - | ||
| - headers[ i - 1 ] = new Action( text, accelerator, HEADER, | ||
| - e -> getActiveEditor().surroundSelection( markup, "", prompt ), | ||
| - activeFileEditorIsNull ); | ||
| - } | ||
| - | ||
| - Action insertUnorderedListAction = new Action( Messages.get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL, | ||
| - e -> getActiveEditor().surroundSelection( "\n\n* ", "" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertOrderedListAction = new Action( Messages.get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL, | ||
| - e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertHorizontalRuleAction = new Action( Messages.get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null, | ||
| - e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ), | ||
| - activeFileEditorIsNull ); | ||
| - | ||
| - // Help actions | ||
| - Action helpAboutAction = new Action( Messages.get( "Main.menu.help.about" ), null, null, e -> helpAbout() ); | ||
| - | ||
| - //---- MenuBar ---- | ||
| - Menu fileMenu = ActionUtils.createMenu( Messages.get( "Main.menu.file" ), | ||
| - fileNewAction, | ||
| - fileOpenAction, | ||
| - null, | ||
| - fileCloseAction, | ||
| - fileCloseAllAction, | ||
| - null, | ||
| - fileSaveAction, | ||
| - fileSaveAllAction, | ||
| - null, | ||
| - fileExitAction ); | ||
| - | ||
| - Menu editMenu = ActionUtils.createMenu( Messages.get( "Main.menu.edit" ), | ||
| - editUndoAction, | ||
| - editRedoAction ); | ||
| - | ||
| - Menu insertMenu = ActionUtils.createMenu( Messages.get( "Main.menu.insert" ), | ||
| - insertBoldAction, | ||
| - insertItalicAction, | ||
| - insertStrikethroughAction, | ||
| - insertBlockquoteAction, | ||
| - insertCodeAction, | ||
| - insertFencedCodeBlockAction, | ||
| - null, | ||
| - insertLinkAction, | ||
| - insertImageAction, | ||
| - null, | ||
| - headers[ 0 ], | ||
| - headers[ 1 ], | ||
| - headers[ 2 ], | ||
| - headers[ 3 ], | ||
| - headers[ 4 ], | ||
| - headers[ 5 ], | ||
| - null, | ||
| - insertUnorderedListAction, | ||
| - insertOrderedListAction, | ||
| - insertHorizontalRuleAction ); | ||
| - | ||
| - Menu helpMenu = ActionUtils.createMenu( Messages.get( "Main.menu.help" ), | ||
| - helpAboutAction ); | ||
| - | ||
| - menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu ); | ||
| - | ||
| - //---- ToolBar ---- | ||
| - ToolBar toolBar = ActionUtils.createToolBar( | ||
| - fileNewAction, | ||
| - fileOpenAction, | ||
| - fileSaveAction, | ||
| - null, | ||
| - editUndoAction, | ||
| - editRedoAction, | ||
| - null, | ||
| - insertBoldAction, | ||
| - insertItalicAction, | ||
| - insertBlockquoteAction, | ||
| - insertCodeAction, | ||
| - insertFencedCodeBlockAction, | ||
| - null, | ||
| - insertLinkAction, | ||
| - insertImageAction, | ||
| - null, | ||
| - headers[ 0 ], | ||
| - null, | ||
| - insertUnorderedListAction, | ||
| - insertOrderedListAction ); | ||
| - | ||
| - return new VBox( menuBar, toolBar ); | ||
| - } | ||
| +import static com.scrivenvar.util.StageState.K_PANE_SPLIT_PREVIEW; | ||
| +import com.scrivenvar.yaml.YamlParser; | ||
| +import com.scrivenvar.yaml.YamlTreeAdapter; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO; | ||
| +import java.io.IOException; | ||
| +import java.io.InputStream; | ||
| +import java.util.Map; | ||
| +import java.util.function.Function; | ||
| +import java.util.prefs.Preferences; | ||
| +import javafx.beans.binding.Bindings; | ||
| +import javafx.beans.binding.BooleanBinding; | ||
| +import javafx.beans.property.BooleanProperty; | ||
| +import javafx.beans.property.SimpleBooleanProperty; | ||
| +import javafx.beans.value.ObservableBooleanValue; | ||
| +import javafx.beans.value.ObservableValue; | ||
| +import javafx.collections.ListChangeListener.Change; | ||
| +import javafx.collections.ObservableList; | ||
| +import javafx.event.Event; | ||
| +import javafx.scene.Node; | ||
| +import javafx.scene.Scene; | ||
| +import javafx.scene.control.Alert; | ||
| +import javafx.scene.control.Alert.AlertType; | ||
| +import javafx.scene.control.Menu; | ||
| +import javafx.scene.control.MenuBar; | ||
| +import javafx.scene.control.SplitPane; | ||
| +import javafx.scene.control.Tab; | ||
| +import javafx.scene.control.ToolBar; | ||
| +import javafx.scene.control.TreeView; | ||
| +import javafx.scene.image.Image; | ||
| +import javafx.scene.image.ImageView; | ||
| +import static javafx.scene.input.KeyCode.ESCAPE; | ||
| +import javafx.scene.input.KeyEvent; | ||
| +import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED; | ||
| +import static javafx.scene.input.KeyEvent.KEY_PRESSED; | ||
| +import javafx.scene.layout.BorderPane; | ||
| +import javafx.scene.layout.VBox; | ||
| +import javafx.stage.Window; | ||
| +import javafx.stage.WindowEvent; | ||
| +import org.fxmisc.richtext.StyleClassedTextArea; | ||
| + | ||
| +/** | ||
| + * Main window containing a tab pane in the center for file editors. | ||
| + * | ||
| + * @author Karl Tauber and White Magic Software, Ltd. | ||
| + */ | ||
| +public class MainWindow { | ||
| + | ||
| + private final Options options = Services.load( Options.class ); | ||
| + | ||
| + private Scene scene; | ||
| + | ||
| + private TreeView<String> treeView; | ||
| + private DefinitionPane definitionPane; | ||
| + private FileEditorTabPane fileEditorPane; | ||
| + private HTMLPreviewPane previewPane; | ||
| + | ||
| + private VariableNameInjector variableNameInjector; | ||
| + | ||
| + private YamlTreeAdapter yamlTreeAdapter; | ||
| + private YamlParser yamlParser; | ||
| + | ||
| + private MenuBar menuBar; | ||
| + | ||
| + public MainWindow() { | ||
| + initLayout(); | ||
| + initTabsListener(); | ||
| + restorePreferences(); | ||
| + initEditorPaneListeners(); | ||
| + initVariableNameInjector(); | ||
| + } | ||
| + | ||
| + private void initLayout() { | ||
| + final SplitPane splitPane = new SplitPane( | ||
| + getDefinitionPane().getNode(), | ||
| + getFileEditorPane().getNode(), | ||
| + getPreviewPane().getNode() ); | ||
| + | ||
| + splitPane.setDividerPositions( | ||
| + getFloat( K_PANE_SPLIT_DEFINITION, .10f ), | ||
| + getFloat( K_PANE_SPLIT_EDITOR, .45f ), | ||
| + getFloat( K_PANE_SPLIT_PREVIEW, .45f ) ); | ||
| + | ||
| + // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html | ||
| + final BorderPane borderPane = new BorderPane(); | ||
| + borderPane.setPrefSize( 1024, 800 ); | ||
| + borderPane.setTop( createMenuBar() ); | ||
| + borderPane.setCenter( splitPane ); | ||
| + | ||
| + final Scene appScene = new Scene( borderPane ); | ||
| + setScene( appScene ); | ||
| + appScene.getStylesheets().add( Constants.STYLESHEET_PREVIEW ); | ||
| + appScene.windowProperty().addListener( | ||
| + (observable, oldWindow, newWindow) -> { | ||
| + newWindow.setOnCloseRequest( e -> { | ||
| + if( !getFileEditorPane().closeAllEditors() ) { | ||
| + e.consume(); | ||
| + } | ||
| + } ); | ||
| + | ||
| + // Workaround JavaFX bug: deselect menubar if window loses focus. | ||
| + newWindow.focusedProperty().addListener( | ||
| + (obs, oldFocused, newFocused) -> { | ||
| + if( !newFocused ) { | ||
| + // Send an ESC key event to the menubar | ||
| + this.menuBar.fireEvent( | ||
| + new KeyEvent( | ||
| + KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE, | ||
| + false, false, false, false ) ); | ||
| + } | ||
| + } ); | ||
| + } ); | ||
| + } | ||
| + | ||
| + private void initVariableNameInjector() { | ||
| + setVariableNameInjector( new VariableNameInjector( | ||
| + getFileEditorPane(), | ||
| + getDefinitionPane() ) | ||
| + ); | ||
| + } | ||
| + | ||
| + private Window getWindow() { | ||
| + return getScene().getWindow(); | ||
| + } | ||
| + | ||
| + public Scene getScene() { | ||
| + return this.scene; | ||
| + } | ||
| + | ||
| + private void setScene( Scene scene ) { | ||
| + this.scene = scene; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Creates a boolean property that is bound to another boolean value of the | ||
| + * active editor. | ||
| + */ | ||
| + private BooleanProperty createActiveBooleanProperty( | ||
| + final Function<FileEditorTab, ObservableBooleanValue> func ) { | ||
| + | ||
| + final BooleanProperty b = new SimpleBooleanProperty(); | ||
| + final FileEditorTab tab = getActiveFileEditor(); | ||
| + | ||
| + if( tab != null ) { | ||
| + b.bind( func.apply( tab ) ); | ||
| + } | ||
| + | ||
| + getFileEditorPane().activeFileEditorProperty().addListener( | ||
| + (observable, oldFileEditor, newFileEditor) -> { | ||
| + b.unbind(); | ||
| + | ||
| + if( newFileEditor != null ) { | ||
| + b.bind( func.apply( newFileEditor ) ); | ||
| + } else { | ||
| + b.set( false ); | ||
| + } | ||
| + } ); | ||
| + | ||
| + return b; | ||
| + } | ||
| + | ||
| + //---- File actions ------------------------------------------------------- | ||
| + private void fileNew() { | ||
| + getFileEditorPane().newEditor(); | ||
| + } | ||
| + | ||
| + private void fileOpen() { | ||
| + getFileEditorPane().openFileDialog(); | ||
| + } | ||
| + | ||
| + private void fileClose() { | ||
| + getFileEditorPane().closeEditor( getActiveFileEditor(), true ); | ||
| + } | ||
| + | ||
| + private void fileCloseAll() { | ||
| + getFileEditorPane().closeAllEditors(); | ||
| + } | ||
| + | ||
| + private void fileSave() { | ||
| + getFileEditorPane().saveEditor( getActiveFileEditor() ); | ||
| + } | ||
| + | ||
| + private void fileSaveAll() { | ||
| + getFileEditorPane().saveAllEditors(); | ||
| + } | ||
| + | ||
| + private void fileExit() { | ||
| + final Window window = getWindow(); | ||
| + Event.fireEvent( window, | ||
| + new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) ); | ||
| + } | ||
| + | ||
| + //---- Help actions ------------------------------------------------------- | ||
| + private void helpAbout() { | ||
| + Alert alert = new Alert( AlertType.INFORMATION ); | ||
| + alert.setTitle( Messages.get( "Dialog.about.title" ) ); | ||
| + alert.setHeaderText( Messages.get( "Dialog.about.header" ) ); | ||
| + alert.setContentText( Messages.get( "Dialog.about.content" ) ); | ||
| + alert.setGraphic( new ImageView( new Image( LOGO_32 ) ) ); | ||
| + alert.initOwner( getWindow() ); | ||
| + | ||
| + alert.showAndWait(); | ||
| + } | ||
| + | ||
| + private FileEditorTabPane getFileEditorPane() { | ||
| + if( this.fileEditorPane == null ) { | ||
| + this.fileEditorPane = createFileEditorPane(); | ||
| + } | ||
| + | ||
| + return this.fileEditorPane; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Create an editor pane to hold file editor tabs. | ||
| + * | ||
| + * @return A new instance, never null. | ||
| + */ | ||
| + private FileEditorTabPane createFileEditorPane() { | ||
| + return new FileEditorTabPane(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Reloads the preferences from the previous load. | ||
| + */ | ||
| + private void restorePreferences() { | ||
| + getFileEditorPane().restorePreferences(); | ||
| + } | ||
| + | ||
| + private void initTabsListener() { | ||
| + final FileEditorTabPane editorPane = getFileEditorPane(); | ||
| + | ||
| + // Make sure the text processor kicks off when new files are opened. | ||
| + final ObservableList<Tab> tabs = editorPane.getTabs(); | ||
| + | ||
| + // Update the preview pane on tab changes. | ||
| + tabs.addListener( (final Change<? extends Tab> change) -> { | ||
| + while( change.next() ) { | ||
| + if( change.wasAdded() ) { | ||
| + // Multiple tabs can be added simultaneously. | ||
| + for( final Tab newTab : change.getAddedSubList() ) { | ||
| + final FileEditorTab tab = (FileEditorTab)newTab; | ||
| + | ||
| + refresh( tab ); | ||
| + } | ||
| + } | ||
| + } | ||
| + } ); | ||
| + } | ||
| + | ||
| + private void initEditorPaneListeners() { | ||
| + final FileEditorTabPane editorPane = getFileEditorPane(); | ||
| + | ||
| + // Update the preview pane when moving the caret to a new paragraph. | ||
| + editorPane.addTabChangeListener( | ||
| + (ObservableValue<? extends Tab> tabPane, | ||
| + final Tab oldTab, final Tab newTab) -> { | ||
| + | ||
| + final FileEditorTab tab = (FileEditorTab)newTab; | ||
| + | ||
| + if( tab != null ) { | ||
| + getPreviewPane().setPath( tab.getPath() ); | ||
| + refresh( tab ); | ||
| + } | ||
| + } ); | ||
| + | ||
| + editorPane.getEditor().textProperty().addListener( (ov, oldv, newv) -> { | ||
| + refresh( getActiveFileEditor() ); | ||
| + } ); | ||
| + } | ||
| + | ||
| + private void refresh( final FileEditorTab tab ) { | ||
| + System.out.println( "REFRESH: " + tab.getPath().toAbsolutePath() ); | ||
| + } | ||
| + | ||
| + private MarkdownEditorPane getActiveEditor() { | ||
| + return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane()); | ||
| + } | ||
| + | ||
| + private FileEditorTab getActiveFileEditor() { | ||
| + return getFileEditorPane().getActiveFileEditor(); | ||
| + } | ||
| + | ||
| + private Processor<String> createVariableProcessor( final FileEditorTab tab ) { | ||
| + final HTMLPreviewPane previewPanel = getPreviewPane(); | ||
| + final EditorPane editorPanel = tab.getEditorPane(); | ||
| + final StyleClassedTextArea editor = editorPanel.getEditor(); | ||
| + | ||
| + // TODO: Use a factory based on the filename extension. The default | ||
| + // extension will be for a markdown file (e.g., on file new). | ||
| + final Processor<String> hpp = new HTMLPreviewProcessor( previewPanel ); | ||
| + final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp ); | ||
| + final Processor<String> mp = new MarkdownProcessor( mcrp ); | ||
| + final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, editor.caretPositionProperty() ); | ||
| + final Processor<String> vp = new VariableProcessor( mcip, getResolvedMap() ); | ||
| + | ||
| + return vp; | ||
| + } | ||
| + | ||
| + private TextChangeProcessor createTextChangeProcessor( | ||
| + final Processor<String> link ) { | ||
| + return new TextChangeProcessor( link ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Listens for changes to tabs and their text editors. | ||
| + * | ||
| + * @see https://github.com/DaveJarvis/scrivenvar/issues/17 | ||
| + * @see https://github.com/DaveJarvis/scrivenvar/issues/18 | ||
| + * | ||
| + * @param tab The file editor tab that contains a text editor. | ||
| + */ | ||
| + private void addListener( final FileEditorTab tab ) { | ||
| + final Processor<String> vnp = createVariableProcessor( tab ); | ||
| + final TextChangeProcessor tcp = createTextChangeProcessor( vnp ); | ||
| + | ||
| + addCaretParagraphListener( tab, vnp ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * When the caret changes paragraph, force re-rendering of the preview panel | ||
| + * using the chain-of-command. | ||
| + * | ||
| + * @param tab Contains a text editor to monitor for caret position changes. | ||
| + * @param vnp Called to re-process chain using the editor's content. | ||
| + */ | ||
| + private void addCaretParagraphListener( final FileEditorTab tab, final Processor<String> vnp ) { | ||
| + final EditorPane editorPanel = tab.getEditorPane(); | ||
| + final StyleClassedTextArea editor = editorPanel.getEditor(); | ||
| + | ||
| + editorPanel.addCaretParagraphListener( | ||
| + (final ObservableValue<? extends Integer> observable, | ||
| + final Integer oldValue, final Integer newValue) -> { | ||
| + | ||
| + // Kick off the processing chain at the variable processor when the | ||
| + // cursor changes paragraphs. This might cause some slight duplication | ||
| + // when the Enter key is pressed. | ||
| + vnp.processChain( editor.getText() ); | ||
| + } ); | ||
| + } | ||
| + | ||
| + protected DefinitionPane createDefinitionPane() { | ||
| + return new DefinitionPane( getTreeView() ); | ||
| + } | ||
| + | ||
| + private DefinitionPane getDefinitionPane() { | ||
| + if( this.definitionPane == null ) { | ||
| + this.definitionPane = createDefinitionPane(); | ||
| + } | ||
| + | ||
| + return this.definitionPane; | ||
| + } | ||
| + | ||
| + public MenuBar getMenuBar() { | ||
| + return menuBar; | ||
| + } | ||
| + | ||
| + public void setMenuBar( MenuBar menuBar ) { | ||
| + this.menuBar = menuBar; | ||
| + } | ||
| + | ||
| + public VariableNameInjector getVariableNameInjector() { | ||
| + return this.variableNameInjector; | ||
| + } | ||
| + | ||
| + public void setVariableNameInjector( VariableNameInjector variableNameInjector ) { | ||
| + this.variableNameInjector = variableNameInjector; | ||
| + } | ||
| + | ||
| + private float getFloat( final String key, final float defaultValue ) { | ||
| + return getPreferences().getFloat( key, defaultValue ); | ||
| + } | ||
| + | ||
| + private Preferences getPreferences() { | ||
| + return getOptions().getState(); | ||
| + } | ||
| + | ||
| + private Options getOptions() { | ||
| + return this.options; | ||
| + } | ||
| + | ||
| + private synchronized TreeView<String> getTreeView() throws RuntimeException { | ||
| + if( this.treeView == null ) { | ||
| + try { | ||
| + this.treeView = createTreeView(); | ||
| + } catch( IOException ex ) { | ||
| + | ||
| + // TODO: Pop an error message. | ||
| + throw new RuntimeException( ex ); | ||
| + } | ||
| + } | ||
| + | ||
| + return this.treeView; | ||
| + } | ||
| + | ||
| + private InputStream asStream( final String resource ) { | ||
| + return getClass().getResourceAsStream( resource ); | ||
| + } | ||
| + | ||
| + private TreeView<String> createTreeView() throws IOException { | ||
| + // TODO: Associate variable file with path to current file. | ||
| + return getYamlTreeAdapter().adapt( | ||
| + asStream( "/com/scrivenvar/variables.yaml" ), | ||
| + get( "Pane.defintion.node.root.title" ) | ||
| + ); | ||
| + } | ||
| + | ||
| + private Map<String, String> getResolvedMap() { | ||
| + return getYamlParser().createResolvedMap(); | ||
| + } | ||
| + | ||
| + private YamlTreeAdapter getYamlTreeAdapter() { | ||
| + if( this.yamlTreeAdapter == null ) { | ||
| + setYamlTreeAdapter( new YamlTreeAdapter( getYamlParser() ) ); | ||
| + } | ||
| + | ||
| + return this.yamlTreeAdapter; | ||
| + } | ||
| + | ||
| + private void setYamlTreeAdapter( final YamlTreeAdapter yamlTreeAdapter ) { | ||
| + this.yamlTreeAdapter = yamlTreeAdapter; | ||
| + } | ||
| + | ||
| + private YamlParser getYamlParser() { | ||
| + if( this.yamlParser == null ) { | ||
| + setYamlParser( new YamlParser() ); | ||
| + } | ||
| + | ||
| + return this.yamlParser; | ||
| + } | ||
| + | ||
| + private void setYamlParser( final YamlParser yamlParser ) { | ||
| + this.yamlParser = yamlParser; | ||
| + } | ||
| + | ||
| + private Node createMenuBar() { | ||
| + final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull(); | ||
| + | ||
| + // File actions | ||
| + Action fileNewAction = new Action( Messages.get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() ); | ||
| + Action fileOpenAction = new Action( Messages.get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() ); | ||
| + Action fileCloseAction = new Action( Messages.get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull ); | ||
| + Action fileCloseAllAction = new Action( Messages.get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull ); | ||
| + Action fileSaveAction = new Action( Messages.get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(), | ||
| + createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() ); | ||
| + Action fileSaveAllAction = new Action( Messages.get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(), | ||
| + Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) ); | ||
| + Action fileExitAction = new Action( Messages.get( "Main.menu.file.exit" ), null, null, e -> fileExit() ); | ||
| + | ||
| + // Edit actions | ||
| + Action editUndoAction = new Action( Messages.get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO, | ||
| + e -> getActiveEditor().undo(), | ||
| + createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() ); | ||
| + Action editRedoAction = new Action( Messages.get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT, | ||
| + e -> getActiveEditor().redo(), | ||
| + createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() ); | ||
| + | ||
| + // Insert actions | ||
| + Action insertBoldAction = new Action( Messages.get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD, | ||
| + e -> getActiveEditor().surroundSelection( "**", "**" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertItalicAction = new Action( Messages.get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC, | ||
| + e -> getActiveEditor().surroundSelection( "*", "*" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertStrikethroughAction = new Action( Messages.get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH, | ||
| + e -> getActiveEditor().surroundSelection( "~~", "~~" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertBlockquoteAction = new Action( Messages.get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac | ||
| + e -> getActiveEditor().surroundSelection( "\n\n> ", "" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertCodeAction = new Action( Messages.get( "Main.menu.insert.code" ), "Shortcut+K", CODE, | ||
| + e -> getActiveEditor().surroundSelection( "`", "`" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertFencedCodeBlockAction = new Action( Messages.get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT, | ||
| + e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", Messages.get( "Main.menu.insert.fenced_code_block.prompt" ) ), | ||
| + activeFileEditorIsNull ); | ||
| + | ||
| + Action insertLinkAction = new Action( Messages.get( "Main.menu.insert.link" ), "Shortcut+L", LINK, | ||
| + e -> getActiveEditor().insertLink(), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertImageAction = new Action( Messages.get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT, | ||
| + e -> getActiveEditor().insertImage(), | ||
| + activeFileEditorIsNull ); | ||
| + | ||
| + final Action[] headers = new Action[ 6 ]; | ||
| + | ||
| + // Insert header actions (H1 ... H6) | ||
| + for( int i = 1; i <= 6; i++ ) { | ||
| + final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | ||
| + final String markup = String.format( "\n\n%s ", hashes ); | ||
| + final String text = Messages.get( "Main.menu.insert.header_" + i ); | ||
| + final String accelerator = "Shortcut+" + i; | ||
| + final String prompt = Messages.get( "Main.menu.insert.header_" + i + ".prompt" ); | ||
| + | ||
| + headers[ i - 1 ] = new Action( text, accelerator, HEADER, | ||
| + e -> getActiveEditor().surroundSelection( markup, "", prompt ), | ||
| + activeFileEditorIsNull ); | ||
| + } | ||
| + | ||
| + Action insertUnorderedListAction = new Action( Messages.get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL, | ||
| + e -> getActiveEditor().surroundSelection( "\n\n* ", "" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertOrderedListAction = new Action( Messages.get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL, | ||
| + e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertHorizontalRuleAction = new Action( Messages.get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null, | ||
| + e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ), | ||
| + activeFileEditorIsNull ); | ||
| + | ||
| + // Help actions | ||
| + Action helpAboutAction = new Action( Messages.get( "Main.menu.help.about" ), null, null, e -> helpAbout() ); | ||
| + | ||
| + //---- MenuBar ---- | ||
| + Menu fileMenu = ActionUtils.createMenu( Messages.get( "Main.menu.file" ), | ||
| + fileNewAction, | ||
| + fileOpenAction, | ||
| + null, | ||
| + fileCloseAction, | ||
| + fileCloseAllAction, | ||
| + null, | ||
| + fileSaveAction, | ||
| + fileSaveAllAction, | ||
| + null, | ||
| + fileExitAction ); | ||
| + | ||
| + Menu editMenu = ActionUtils.createMenu( Messages.get( "Main.menu.edit" ), | ||
| + editUndoAction, | ||
| + editRedoAction ); | ||
| + | ||
| + Menu insertMenu = ActionUtils.createMenu( Messages.get( "Main.menu.insert" ), | ||
| + insertBoldAction, | ||
| + insertItalicAction, | ||
| + insertStrikethroughAction, | ||
| + insertBlockquoteAction, | ||
| + insertCodeAction, | ||
| + insertFencedCodeBlockAction, | ||
| + null, | ||
| + insertLinkAction, | ||
| + insertImageAction, | ||
| + null, | ||
| + headers[ 0 ], | ||
| + headers[ 1 ], | ||
| + headers[ 2 ], | ||
| + headers[ 3 ], | ||
| + headers[ 4 ], | ||
| + headers[ 5 ], | ||
| + null, | ||
| + insertUnorderedListAction, | ||
| + insertOrderedListAction, | ||
| + insertHorizontalRuleAction ); | ||
| + | ||
| + Menu helpMenu = ActionUtils.createMenu( Messages.get( "Main.menu.help" ), | ||
| + helpAboutAction ); | ||
| + | ||
| + menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu ); | ||
| + | ||
| + //---- ToolBar ---- | ||
| + ToolBar toolBar = ActionUtils.createToolBar( | ||
| + fileNewAction, | ||
| + fileOpenAction, | ||
| + fileSaveAction, | ||
| + null, | ||
| + editUndoAction, | ||
| + editRedoAction, | ||
| + null, | ||
| + insertBoldAction, | ||
| + insertItalicAction, | ||
| + insertBlockquoteAction, | ||
| + insertCodeAction, | ||
| + insertFencedCodeBlockAction, | ||
| + null, | ||
| + insertLinkAction, | ||
| + insertImageAction, | ||
| + null, | ||
| + headers[ 0 ], | ||
| + null, | ||
| + insertUnorderedListAction, | ||
| + insertOrderedListAction ); | ||
| + | ||
| + return new VBox( menuBar, toolBar ); | ||
| + } | ||
| + | ||
| + private synchronized HTMLPreviewPane getPreviewPane() { | ||
| + if( this.previewPane == null ) { | ||
| + this.previewPane = new HTMLPreviewPane(); | ||
| + } | ||
| + | ||
| + return this.previewPane; | ||
| + } | ||
| + | ||
| } | ||
| * @param listener Receives paragraph change events. | ||
| */ | ||
| - public void addCaretParagraphListener( final ChangeListener<? super Integer> listener ) { | ||
| + public void addCaretParagraphListener( | ||
| + final ChangeListener<? super Integer> listener ) { | ||
| getEditor().currentParagraphProperty().addListener( listener ); | ||
| } |
| private final WebView webView = new WebView(); | ||
| - private String html; | ||
| private Path path; | ||
| /** | ||
| * Creates a new preview pane that can scroll to the caret position within the | ||
| * document. | ||
| - * | ||
| - * @param path The base path for loading resources, such as images. | ||
| */ | ||
| - public HTMLPreviewPane( final Path path ) { | ||
| - setPath( path ); | ||
| + public HTMLPreviewPane() { | ||
| initListeners(); | ||
| initTraversal(); | ||
| } | ||
| - private void setPath( final Path path ) { | ||
| + public void setPath( final Path path ) { | ||
| this.path = path; | ||
| } | ||
| + /** | ||
| + * Content to embed in a panel. | ||
| + * | ||
| + * @return The content to display to the user. | ||
| + */ | ||
| public Node getNode() { | ||
| return getWebView(); | ||
| import static com.scrivenvar.Constants.MD_CARET_POSITION; | ||
| import static java.lang.Character.isLetter; | ||
| -import org.fxmisc.richtext.model.TextEditingArea; | ||
| +import javafx.beans.value.ObservableValue; | ||
| /** | ||
| public class MarkdownCaretInsertionProcessor extends AbstractProcessor<String> { | ||
| - private TextEditingArea editor; | ||
| + private final ObservableValue<Integer> caretPosition; | ||
| /** | ||
| * Constructs a processor capable of inserting a caret marker into Markdown. | ||
| * | ||
| * @param processor The next processor in the chain. | ||
| - * @param editor The editor that has a caret with a position in the text. | ||
| + * @param position The caret's current position in the text, cannot be null. | ||
| */ | ||
| public MarkdownCaretInsertionProcessor( | ||
| - final Processor<String> processor, final TextEditingArea editor ) { | ||
| + final Processor<String> processor, final ObservableValue<Integer> position ) { | ||
| super( processor ); | ||
| - setEditor( editor ); | ||
| + this.caretPosition = position; | ||
| } | ||
| offset++; | ||
| } | ||
| + | ||
| + // TODO: Ensure that the caret position is outside of an element, | ||
| + // so that a caret inserted in the image doesn't corrupt it. Such as: | ||
| + // | ||
| + //  | ||
| // Insert the caret position into the Markdown text, but don't interfere | ||
| */ | ||
| private int getCaretPosition() { | ||
| - return getEditor().getCaretPosition(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the editor that has a caret position. | ||
| - * | ||
| - * @return An editor with a caret position. | ||
| - */ | ||
| - private TextEditingArea getEditor() { | ||
| - return this.editor; | ||
| - } | ||
| - | ||
| - private void setEditor( final TextEditingArea editor ) { | ||
| - this.editor = editor; | ||
| + return this.caretPosition.getValue(); | ||
| } | ||
| } | ||
| public static final String K_PANE_SPLIT_DEFINITION = "pane.split.definition"; | ||
| public static final String K_PANE_SPLIT_EDITOR = "pane.split.editor"; | ||
| + public static final String K_PANE_SPLIT_PREVIEW = "pane.split.preview"; | ||
| private final Stage stage; |
| Delta | 1484 lines added, 1428 lines removed, 56-line increase |
|---|