| Author | djarvis <email> |
|---|---|
| Date | 2016-12-27 22:16:07 GMT-0800 |
| Commit | 6841785c3ea1401c0a50ecb02ebecc9c6e20c3e9 |
| Parent | f38bac3 |
| -version = '1.0.7' | ||
| +version = '1.0.8' | ||
| apply plugin: 'java' | ||
| dependencies { | ||
| + compile 'org.controlsfx:controlsfx:8.40.12' | ||
| compile 'org.fxmisc.richtext:richtextfx:0.7-M2' | ||
| compile 'com.miglayout:miglayout-javafx:5.0' | ||
| * @return The file type that corresponds to the given path. | ||
| */ | ||
| - protected FileType lookup( final Path path, final String prefix ) { | ||
| + protected FileType lookup( final Path path, final String prefix ) { | ||
| final Settings properties = getSettings(); | ||
| final Iterator<String> keys = properties.getKeys( prefix ); | ||
| return this.settings; | ||
| } | ||
| - | ||
| } | ||
| import com.scrivenvar.editors.markdown.MarkdownEditorPane; | ||
| import com.scrivenvar.service.events.Notification; | ||
| -import com.scrivenvar.service.events.NotifyService; | ||
| -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.beans.value.ChangeListener; | ||
| -import javafx.beans.value.ObservableValue; | ||
| -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.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 NotifyService alertService = Services.load( NotifyService.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(); | ||
| - | ||
| - // Might be simpler to revert this back to a property and have the main | ||
| - // window listen for changes to it... | ||
| - private Path path; | ||
| - | ||
| - FileEditorTab( final Path path ) { | ||
| - setPath( path ); | ||
| - | ||
| - 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 new Tooltip( filePath == null ? "" : 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 -- can only 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 getEditor().getCaretPosition(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Allows observers to synchronize caret position changes. | ||
| - * | ||
| - * @return An observable caret property value. | ||
| - */ | ||
| - public final ObservableValue<Integer> caretPositionProperty() { | ||
| - return getEditor().caretPositionProperty(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the text area associated with this tab. | ||
| - * | ||
| - * @return A text editor. | ||
| - */ | ||
| - private StyleClassedTextArea getEditor() { | ||
| - return getEditorPane().getEditor(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * 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 NotifyService service = getAlertService(); | ||
| - final Path filePath = getPath(); | ||
| - | ||
| - final Notification message = service.createNotification( | ||
| - Messages.get( titleKey ), | ||
| - Messages.get( messageKey ), | ||
| - filePath == null ? "" : filePath, | ||
| - e.getMessage() | ||
| - ); | ||
| - | ||
| - // TODO: Put this into a status bar or status area | ||
| - System.out.println( e ); | ||
| - | ||
| -// service.createError( 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() ); | ||
| - } | ||
| - | ||
| - public Path getPath() { | ||
| - return this.path; | ||
| - } | ||
| - | ||
| - public void setPath( final Path path ) { | ||
| - this.path = path; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Answers whether this tab has an initialized path reference. | ||
| - * | ||
| - * @return false This tab has no path. | ||
| - */ | ||
| - public boolean isFileOpen() { | ||
| - return this.path != null; | ||
| - } | ||
| - | ||
| - 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 ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Forwards to the editor pane's listeners for text change events. | ||
| - * | ||
| - * @param listener The listener to notify when the text changes. | ||
| - */ | ||
| - public void addTextChangeListener( final ChangeListener<String> listener ) { | ||
| - getEditorPane().addTextChangeListener( listener ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Forwards to the editor pane's listeners for caret paragraph change events. | ||
| - * | ||
| - * @param listener The listener to notify when the caret changes paragraphs. | ||
| - */ | ||
| - public void addCaretParagraphListener( final ChangeListener<Integer> listener ) { | ||
| - getEditorPane().addCaretParagraphListener( listener ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Forwards the request to the editor pane. | ||
| - * | ||
| - * @return The text to process. | ||
| - */ | ||
| - public String getEditorText() { | ||
| - return getEditorPane().getText(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the editor pane, or creates one if it doesn't yet exist. | ||
| - * | ||
| - * @return The editor pane, never null. | ||
| - */ | ||
| - public EditorPane getEditorPane() { | ||
| - if( this.editorPane == null ) { | ||
| - this.editorPane = new MarkdownEditorPane(); | ||
| - } | ||
| - | ||
| - return this.editorPane; | ||
| - } | ||
| - | ||
| - private NotifyService getAlertService() { | ||
| +import com.scrivenvar.service.events.Notifier; | ||
| +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.beans.value.ChangeListener; | ||
| +import javafx.beans.value.ObservableValue; | ||
| +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 javafx.stage.Window; | ||
| +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 Notifier alertService = Services.load(Notifier.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(); | ||
| + | ||
| + // Might be simpler to revert this back to a property and have the main | ||
| + // window listen for changes to it... | ||
| + private Path path; | ||
| + | ||
| + FileEditorTab( final Path path ) { | ||
| + setPath( path ); | ||
| + | ||
| + 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 new Tooltip( filePath == null ? "" : 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 -- can only 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 getEditor().getCaretPosition(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Allows observers to synchronize caret position changes. | ||
| + * | ||
| + * @return An observable caret property value. | ||
| + */ | ||
| + public final ObservableValue<Integer> caretPositionProperty() { | ||
| + return getEditor().caretPositionProperty(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the text area associated with this tab. | ||
| + * | ||
| + * @return A text editor. | ||
| + */ | ||
| + private StyleClassedTextArea getEditor() { | ||
| + return getEditorPane().getEditor(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * 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( final Exception ex ) { | ||
| + getNotifyService().notify( 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( final 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 Notifier service = getNotifyService(); | ||
| + final Path filePath = getPath(); | ||
| + | ||
| + final Notification message = service.createNotification( | ||
| + Messages.get( titleKey ), | ||
| + Messages.get( messageKey ), | ||
| + filePath == null ? "" : filePath, | ||
| + e.getMessage() | ||
| + ); | ||
| + | ||
| + service.createError( getWindow(), message ).showAndWait(); | ||
| + return false; | ||
| + } | ||
| + | ||
| + private Window getWindow() { | ||
| + return getEditorPane().getScene().getWindow(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * 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() ); | ||
| + } | ||
| + | ||
| + public Path getPath() { | ||
| + return this.path; | ||
| + } | ||
| + | ||
| + public void setPath( final Path path ) { | ||
| + this.path = path; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Answers whether this tab has an initialized path reference. | ||
| + * | ||
| + * @return false This tab has no path. | ||
| + */ | ||
| + public boolean isFileOpen() { | ||
| + return this.path != null; | ||
| + } | ||
| + | ||
| + 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 ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Forwards to the editor pane's listeners for text change events. | ||
| + * | ||
| + * @param listener The listener to notify when the text changes. | ||
| + */ | ||
| + public void addTextChangeListener( final ChangeListener<String> listener ) { | ||
| + getEditorPane().addTextChangeListener( listener ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Forwards to the editor pane's listeners for caret paragraph change events. | ||
| + * | ||
| + * @param listener The listener to notify when the caret changes paragraphs. | ||
| + */ | ||
| + public void addCaretParagraphListener( final ChangeListener<Integer> listener ) { | ||
| + getEditorPane().addCaretParagraphListener( listener ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Forwards the request to the editor pane. | ||
| + * | ||
| + * @return The text to process. | ||
| + */ | ||
| + public String getEditorText() { | ||
| + return getEditorPane().getText(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the editor pane, or creates one if it doesn't yet exist. | ||
| + * | ||
| + * @return The editor pane, never null. | ||
| + */ | ||
| + public EditorPane getEditorPane() { | ||
| + if( this.editorPane == null ) { | ||
| + this.editorPane = new MarkdownEditorPane(); | ||
| + } | ||
| + | ||
| + return this.editorPane; | ||
| + } | ||
| + | ||
| + private Notifier getNotifyService() { | ||
| return this.alertService; | ||
| } |
| import com.scrivenvar.service.Settings; | ||
| import com.scrivenvar.service.events.Notification; | ||
| -import com.scrivenvar.service.events.NotifyService; | ||
| -import static com.scrivenvar.service.events.NotifyService.NO; | ||
| -import static com.scrivenvar.service.events.NotifyService.YES; | ||
| -import com.scrivenvar.util.Utils; | ||
| -import java.io.File; | ||
| -import java.nio.file.Path; | ||
| -import java.util.ArrayList; | ||
| -import java.util.List; | ||
| -import java.util.function.Consumer; | ||
| -import java.util.prefs.Preferences; | ||
| -import java.util.stream.Collectors; | ||
| -import javafx.beans.property.ReadOnlyBooleanProperty; | ||
| -import javafx.beans.property.ReadOnlyBooleanWrapper; | ||
| -import javafx.beans.property.ReadOnlyObjectProperty; | ||
| -import javafx.beans.property.ReadOnlyObjectWrapper; | ||
| -import javafx.beans.value.ChangeListener; | ||
| -import javafx.beans.value.ObservableValue; | ||
| -import javafx.collections.ListChangeListener; | ||
| -import javafx.collections.ObservableList; | ||
| -import javafx.event.Event; | ||
| -import javafx.scene.Node; | ||
| -import javafx.scene.control.Alert; | ||
| -import javafx.scene.control.ButtonType; | ||
| -import javafx.scene.control.Tab; | ||
| -import javafx.scene.control.TabPane; | ||
| -import javafx.scene.control.TabPane.TabClosingPolicy; | ||
| -import javafx.scene.input.InputEvent; | ||
| -import javafx.stage.FileChooser; | ||
| -import javafx.stage.FileChooser.ExtensionFilter; | ||
| -import javafx.stage.Window; | ||
| -import org.fxmisc.richtext.StyledTextArea; | ||
| -import org.fxmisc.wellbehaved.event.EventPattern; | ||
| -import org.fxmisc.wellbehaved.event.InputMap; | ||
| - | ||
| -/** | ||
| - * Tab pane for file editors. | ||
| - * | ||
| - * @author Karl Tauber and White Magic Software, Ltd. | ||
| - */ | ||
| -public final class FileEditorTabPane extends TabPane { | ||
| - | ||
| - private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose.filter"; | ||
| - | ||
| - private final Options options = Services.load( Options.class ); | ||
| - private final Settings settings = Services.load( Settings.class ); | ||
| - private final NotifyService alertService = Services.load(NotifyService.class ); | ||
| - | ||
| - private final ReadOnlyObjectWrapper<Path> openDefinition = new ReadOnlyObjectWrapper<>(); | ||
| - private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>(); | ||
| - private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper(); | ||
| - | ||
| - /** | ||
| - * Constructs a new file editor tab pane. | ||
| - */ | ||
| - public FileEditorTabPane() { | ||
| - final ObservableList<Tab> tabs = getTabs(); | ||
| - | ||
| - setFocusTraversable( false ); | ||
| - setTabClosingPolicy( TabClosingPolicy.ALL_TABS ); | ||
| - | ||
| - addTabSelectionListener( | ||
| - (ObservableValue<? extends Tab> tabPane, | ||
| - final Tab oldTab, final Tab newTab) -> { | ||
| - | ||
| - if( newTab != null ) { | ||
| - activeFileEditor.set( (FileEditorTab)newTab ); | ||
| - } | ||
| - } | ||
| - ); | ||
| - | ||
| - final ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> { | ||
| - for( final Tab tab : tabs ) { | ||
| - if( ((FileEditorTab)tab).isModified() ) { | ||
| - this.anyFileEditorModified.set( true ); | ||
| - break; | ||
| - } | ||
| - } | ||
| - }; | ||
| - | ||
| - tabs.addListener( | ||
| - (ListChangeListener<Tab>)change -> { | ||
| - while( change.next() ) { | ||
| - if( change.wasAdded() ) { | ||
| - change.getAddedSubList().stream().forEach( (tab) -> { | ||
| - ((FileEditorTab)tab).modifiedProperty().addListener( modifiedListener ); | ||
| - } ); | ||
| - } else if( change.wasRemoved() ) { | ||
| - change.getRemoved().stream().forEach( (tab) -> { | ||
| - ((FileEditorTab)tab).modifiedProperty().removeListener( modifiedListener ); | ||
| - } ); | ||
| - } | ||
| - } | ||
| - | ||
| - // Changes in the tabs may also change anyFileEditorModified property | ||
| - // (e.g. closed modified file) | ||
| - modifiedListener.changed( null, null, null ); | ||
| - } | ||
| - ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Delegates to the active file editor. | ||
| - * | ||
| - * @param <T> Event type. | ||
| - * @param <U> Consumer type. | ||
| - * @param event Event to pass to the editor. | ||
| - * @param consumer Consumer to pass to the editor. | ||
| - */ | ||
| - 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 addTabSelectionListener( final ChangeListener<Tab> listener ) { | ||
| - // Observe the tab so that when a new tab is opened or selected, | ||
| - // a notification is kicked off. | ||
| - getSelectionModel().selectedItemProperty().addListener( listener ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Allows clients to manipulate the editor content directly. | ||
| - * | ||
| - * @return The text area for the active file editor. | ||
| - */ | ||
| - public StyledTextArea getEditor() { | ||
| - return getActiveFileEditor().getEditorPane().getEditor(); | ||
| - } | ||
| - | ||
| - public FileEditorTab getActiveFileEditor() { | ||
| - return this.activeFileEditor.get(); | ||
| - } | ||
| - | ||
| - public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() { | ||
| - return this.activeFileEditor.getReadOnlyProperty(); | ||
| - } | ||
| - | ||
| - ReadOnlyBooleanProperty anyFileEditorModifiedProperty() { | ||
| - return this.anyFileEditorModified.getReadOnlyProperty(); | ||
| - } | ||
| - | ||
| - private FileEditorTab createFileEditor( final Path path ) { | ||
| - final FileEditorTab tab = new FileEditorTab( path ); | ||
| - | ||
| - tab.setOnCloseRequest( e -> { | ||
| - if( !canCloseEditor( tab ) ) { | ||
| - e.consume(); | ||
| - } | ||
| - } ); | ||
| - | ||
| - return tab; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Called when the user selects New from the File menu. | ||
| - * | ||
| - * @return The newly added tab. | ||
| - */ | ||
| - void newEditor() { | ||
| - final FileEditorTab tab = createFileEditor( null ); | ||
| - | ||
| - getTabs().add( tab ); | ||
| - getSelectionModel().select( tab ); | ||
| - } | ||
| - | ||
| - void openFileDialog() { | ||
| - final String title = get( "Dialog.file.choose.open.title" ); | ||
| - final FileChooser dialog = createFileChooser( title ); | ||
| - final List<File> files = dialog.showOpenMultipleDialog( getWindow() ); | ||
| - | ||
| - if( files != null ) { | ||
| - openFiles( files ); | ||
| - } | ||
| - } | ||
| - | ||
| - /** | ||
| - * Opens the files into new editors, unless one of those files was a | ||
| - * definition file. The definition file is loaded into the definition pane, | ||
| - * but only the first one selected (multiple definition files will result in a | ||
| - * warning). | ||
| - * | ||
| - * @param files The list of non-definition files that the were requested to | ||
| - * open. | ||
| - * | ||
| - * @return A list of files that can be opened in text editors. | ||
| - */ | ||
| - private void openFiles( final List<File> files ) { | ||
| - final FileTypePredicate predicate | ||
| - = new FileTypePredicate( createExtensionFilter( DEFINITION ).getExtensions() ); | ||
| - | ||
| - // The user might have opened multiple definitions files. These will | ||
| - // be discarded from the text editable files. | ||
| - final List<File> definitions | ||
| - = files.stream().filter( predicate ).collect( Collectors.toList() ); | ||
| - | ||
| - // Create a modifiable list to remove any definition files that were | ||
| - // opened. | ||
| - final List<File> editors = new ArrayList<>( files ); | ||
| - | ||
| - if( editors.size() > 0 ) { | ||
| - saveLastDirectory( editors.get( 0 ) ); | ||
| - } | ||
| - | ||
| - editors.removeAll( definitions ); | ||
| - | ||
| - // Open editor-friendly files (e.g,. Markdown, XML) in new tabs. | ||
| - if( editors.size() > 0 ) { | ||
| - openEditors( editors, 0 ); | ||
| - } | ||
| - | ||
| - if( definitions.size() > 0 ) { | ||
| - openDefinition( definitions.get( 0 ) ); | ||
| - } | ||
| - } | ||
| - | ||
| - private void openEditors( final List<File> files, final int activeIndex ) { | ||
| - final int fileTally = files.size(); | ||
| - final List<Tab> tabs = getTabs(); | ||
| - | ||
| - // Close single unmodified "Untitled" tab. | ||
| - if( tabs.size() == 1 ) { | ||
| - final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 )); | ||
| - | ||
| - if( fileEditor.getPath() == null && !fileEditor.isModified() ) { | ||
| - closeEditor( fileEditor, false ); | ||
| - } | ||
| - } | ||
| - | ||
| - for( int i = 0; i < fileTally; i++ ) { | ||
| - final Path path = files.get( i ).toPath(); | ||
| - | ||
| - FileEditorTab fileEditorTab = findEditor( path ); | ||
| - | ||
| - // Only open new files. | ||
| - if( fileEditorTab == null ) { | ||
| - fileEditorTab = createFileEditor( path ); | ||
| - getTabs().add( fileEditorTab ); | ||
| - } | ||
| - | ||
| - // Select the first file in the list. | ||
| - if( i == activeIndex ) { | ||
| - getSelectionModel().select( fileEditorTab ); | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns a property that changes when a new definition file is opened. | ||
| - * | ||
| - * @return The path to a definition file that was opened. | ||
| - */ | ||
| - public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() { | ||
| - return getOnOpenDefinitionFile().getReadOnlyProperty(); | ||
| - } | ||
| - | ||
| - private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() { | ||
| - return this.openDefinition; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Called when the user has opened a definition file (using the file open | ||
| - * dialog box). This will replace the current set of definitions for the | ||
| - * active tab. | ||
| - * | ||
| - * @param definition The file to open. | ||
| - */ | ||
| - private void openDefinition( final File definition ) { | ||
| - // TODO: Prevent reading this file twice when a new text document is opened. | ||
| - // (might be a matter of checking the value first). | ||
| - getOnOpenDefinitionFile().set( definition.toPath() ); | ||
| - } | ||
| - | ||
| - 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; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Answers whether the file has had modifications. ' | ||
| - * | ||
| - * @param tab THe tab to check for modifications. | ||
| - * | ||
| - * @return false The file is unmodified. | ||
| - */ | ||
| - boolean canCloseEditor( final FileEditorTab tab ) { | ||
| - if( !tab.isModified() ) { | ||
| - return true; | ||
| - } | ||
| - | ||
| - final Notification message = getAlertService().createNotification( | ||
| - Messages.get( "Alert.file.close.title" ), | ||
| - Messages.get( "Alert.file.close.text" ), | ||
| - tab.getText() | ||
| - ); | ||
| - | ||
| - final Alert alert = getAlertService().createConfirmation( message ); | ||
| - final ButtonType response = alert.showAndWait().get(); | ||
| - | ||
| - return response == YES ? saveEditor( tab ) : response == NO; | ||
| - } | ||
| - | ||
| - private NotifyService getAlertService() { | ||
| - return this.alertService; | ||
| - } | ||
| - | ||
| - boolean closeEditor( FileEditorTab fileEditor, boolean save ) { | ||
| - if( fileEditor == null ) { | ||
| - return true; | ||
| - } | ||
| - | ||
| - final Tab tab = fileEditor; | ||
| - | ||
| - if( save ) { | ||
| - Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT ); | ||
| - Event.fireEvent( tab, event ); | ||
| - | ||
| - if( event.isConsumed() ) { | ||
| - return false; | ||
| - } | ||
| - } | ||
| - | ||
| - getTabs().remove( tab ); | ||
| - | ||
| - if( tab.getOnClosed() != null ) { | ||
| - Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) ); | ||
| - } | ||
| - | ||
| - return true; | ||
| - } | ||
| - | ||
| - boolean closeAllEditors() { | ||
| - final FileEditorTab[] allEditors = getAllEditors(); | ||
| - final FileEditorTab activeEditor = getActiveFileEditor(); | ||
| - | ||
| - // try to save active tab first because in case the user decides to cancel, | ||
| - // then it stays active | ||
| - if( activeEditor != null && !canCloseEditor( activeEditor ) ) { | ||
| - return false; | ||
| - } | ||
| - | ||
| - // This should be called any time a tab changes. | ||
| - persistPreferences(); | ||
| - | ||
| - // save modified tabs | ||
| - for( int i = 0; i < allEditors.length; i++ ) { | ||
| - final FileEditorTab fileEditor = allEditors[ i ]; | ||
| - | ||
| - if( fileEditor == activeEditor ) { | ||
| - continue; | ||
| - } | ||
| - | ||
| - if( fileEditor.isModified() ) { | ||
| - // activate the modified tab to make its modified content visible to the user | ||
| - getSelectionModel().select( i ); | ||
| - | ||
| - if( !canCloseEditor( fileEditor ) ) { | ||
| - return false; | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - // Close all tabs. | ||
| - for( final FileEditorTab fileEditor : allEditors ) { | ||
| - if( !closeEditor( fileEditor, false ) ) { | ||
| - return false; | ||
| - } | ||
| - } | ||
| - | ||
| - return getTabs().isEmpty(); | ||
| - } | ||
| - | ||
| - private FileEditorTab[] getAllEditors() { | ||
| - final ObservableList<Tab> tabs = getTabs(); | ||
| - final int length = tabs.size(); | ||
| - final FileEditorTab[] allEditors = new FileEditorTab[ length ]; | ||
| - | ||
| - for( int i = 0; i < length; i++ ) { | ||
| - allEditors[ i ] = (FileEditorTab)tabs.get( i ); | ||
| - } | ||
| - | ||
| - return allEditors; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the file editor tab that has the given path. | ||
| - * | ||
| - * @return null No file editor tab for the given path was found. | ||
| - */ | ||
| - private FileEditorTab findEditor( final Path path ) { | ||
| - for( final Tab tab : getTabs() ) { | ||
| - final FileEditorTab fileEditor = (FileEditorTab)tab; | ||
| - | ||
| - if( fileEditor.isPath( path ) ) { | ||
| - return fileEditor; | ||
| - } | ||
| - } | ||
| - | ||
| - return null; | ||
| - } | ||
| - | ||
| - private FileChooser createFileChooser( String title ) { | ||
| - final FileChooser fileChooser = new FileChooser(); | ||
| - | ||
| - fileChooser.setTitle( title ); | ||
| - fileChooser.getExtensionFilters().addAll( | ||
| - createExtensionFilters() ); | ||
| - | ||
| - final String lastDirectory = getPreferences().get( "lastDirectory", null ); | ||
| - File file = new File( (lastDirectory != null) ? lastDirectory : "." ); | ||
| - | ||
| - if( !file.isDirectory() ) { | ||
| - file = new File( "." ); | ||
| - } | ||
| - | ||
| - fileChooser.setInitialDirectory( file ); | ||
| - return fileChooser; | ||
| - } | ||
| - | ||
| - private List<ExtensionFilter> createExtensionFilters() { | ||
| - final List<ExtensionFilter> list = new ArrayList<>(); | ||
| - | ||
| - // TODO: Return a list of all properties that match the filter prefix. | ||
| - // This will allow dynamic filters to be added and removed just by | ||
| - // updating the properties file. | ||
| - list.add( createExtensionFilter( MARKDOWN ) ); | ||
| - list.add( createExtensionFilter( DEFINITION ) ); | ||
| - list.add( createExtensionFilter( XML ) ); | ||
| - list.add( createExtensionFilter( ALL ) ); | ||
| - return list; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns a filter for file name extensions recognized by the application | ||
| - * that can be opened by the user. | ||
| - * | ||
| - * @param filetype Used to find the globbing pattern for extensions. | ||
| - * | ||
| - * @return A filename filter suitable for use by a FileDialog instance. | ||
| - */ | ||
| - private ExtensionFilter createExtensionFilter( final FileType filetype ) { | ||
| - final String tKey = String.format( "%s.title.%s", FILTER_EXTENSION_TITLES, filetype ); | ||
| - final String eKey = String.format("%s.%s", GLOB_PREFIX_FILE, filetype ); | ||
| +import static com.scrivenvar.service.events.Notifier.NO; | ||
| +import static com.scrivenvar.service.events.Notifier.YES; | ||
| +import com.scrivenvar.util.Utils; | ||
| +import java.io.File; | ||
| +import java.nio.file.Path; | ||
| +import java.util.ArrayList; | ||
| +import java.util.List; | ||
| +import java.util.function.Consumer; | ||
| +import java.util.prefs.Preferences; | ||
| +import java.util.stream.Collectors; | ||
| +import javafx.beans.property.ReadOnlyBooleanProperty; | ||
| +import javafx.beans.property.ReadOnlyBooleanWrapper; | ||
| +import javafx.beans.property.ReadOnlyObjectProperty; | ||
| +import javafx.beans.property.ReadOnlyObjectWrapper; | ||
| +import javafx.beans.value.ChangeListener; | ||
| +import javafx.beans.value.ObservableValue; | ||
| +import javafx.collections.ListChangeListener; | ||
| +import javafx.collections.ObservableList; | ||
| +import javafx.event.Event; | ||
| +import javafx.scene.Node; | ||
| +import javafx.scene.control.Alert; | ||
| +import javafx.scene.control.ButtonType; | ||
| +import javafx.scene.control.Tab; | ||
| +import javafx.scene.control.TabPane; | ||
| +import javafx.scene.control.TabPane.TabClosingPolicy; | ||
| +import javafx.scene.input.InputEvent; | ||
| +import javafx.stage.FileChooser; | ||
| +import javafx.stage.FileChooser.ExtensionFilter; | ||
| +import javafx.stage.Window; | ||
| +import org.fxmisc.richtext.StyledTextArea; | ||
| +import org.fxmisc.wellbehaved.event.EventPattern; | ||
| +import org.fxmisc.wellbehaved.event.InputMap; | ||
| +import com.scrivenvar.service.events.Notifier; | ||
| +import static com.scrivenvar.Messages.get; | ||
| + | ||
| +/** | ||
| + * Tab pane for file editors. | ||
| + * | ||
| + * @author Karl Tauber and White Magic Software, Ltd. | ||
| + */ | ||
| +public final class FileEditorTabPane extends TabPane { | ||
| + | ||
| + private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose.filter"; | ||
| + | ||
| + private final Options options = Services.load( Options.class ); | ||
| + private final Settings settings = Services.load( Settings.class ); | ||
| + private final Notifier notifyService = Services.load(Notifier.class ); | ||
| + | ||
| + private final ReadOnlyObjectWrapper<Path> openDefinition = new ReadOnlyObjectWrapper<>(); | ||
| + private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>(); | ||
| + private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper(); | ||
| + | ||
| + /** | ||
| + * Constructs a new file editor tab pane. | ||
| + */ | ||
| + public FileEditorTabPane() { | ||
| + final ObservableList<Tab> tabs = getTabs(); | ||
| + | ||
| + setFocusTraversable( false ); | ||
| + setTabClosingPolicy( TabClosingPolicy.ALL_TABS ); | ||
| + | ||
| + addTabSelectionListener( | ||
| + (ObservableValue<? extends Tab> tabPane, | ||
| + final Tab oldTab, final Tab newTab) -> { | ||
| + | ||
| + if( newTab != null ) { | ||
| + activeFileEditor.set( (FileEditorTab)newTab ); | ||
| + } | ||
| + } | ||
| + ); | ||
| + | ||
| + final ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> { | ||
| + for( final Tab tab : tabs ) { | ||
| + if( ((FileEditorTab)tab).isModified() ) { | ||
| + this.anyFileEditorModified.set( true ); | ||
| + break; | ||
| + } | ||
| + } | ||
| + }; | ||
| + | ||
| + tabs.addListener( | ||
| + (ListChangeListener<Tab>)change -> { | ||
| + while( change.next() ) { | ||
| + if( change.wasAdded() ) { | ||
| + change.getAddedSubList().stream().forEach( (tab) -> { | ||
| + ((FileEditorTab)tab).modifiedProperty().addListener( modifiedListener ); | ||
| + } ); | ||
| + } else if( change.wasRemoved() ) { | ||
| + change.getRemoved().stream().forEach( (tab) -> { | ||
| + ((FileEditorTab)tab).modifiedProperty().removeListener( modifiedListener ); | ||
| + } ); | ||
| + } | ||
| + } | ||
| + | ||
| + // Changes in the tabs may also change anyFileEditorModified property | ||
| + // (e.g. closed modified file) | ||
| + modifiedListener.changed( null, null, null ); | ||
| + } | ||
| + ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Delegates to the active file editor. | ||
| + * | ||
| + * @param <T> Event type. | ||
| + * @param <U> Consumer type. | ||
| + * @param event Event to pass to the editor. | ||
| + * @param consumer Consumer to pass to the editor. | ||
| + */ | ||
| + 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 addTabSelectionListener( final ChangeListener<Tab> listener ) { | ||
| + // Observe the tab so that when a new tab is opened or selected, | ||
| + // a notification is kicked off. | ||
| + getSelectionModel().selectedItemProperty().addListener( listener ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Allows clients to manipulate the editor content directly. | ||
| + * | ||
| + * @return The text area for the active file editor. | ||
| + */ | ||
| + public StyledTextArea getEditor() { | ||
| + return getActiveFileEditor().getEditorPane().getEditor(); | ||
| + } | ||
| + | ||
| + public FileEditorTab getActiveFileEditor() { | ||
| + return this.activeFileEditor.get(); | ||
| + } | ||
| + | ||
| + public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() { | ||
| + return this.activeFileEditor.getReadOnlyProperty(); | ||
| + } | ||
| + | ||
| + ReadOnlyBooleanProperty anyFileEditorModifiedProperty() { | ||
| + return this.anyFileEditorModified.getReadOnlyProperty(); | ||
| + } | ||
| + | ||
| + private FileEditorTab createFileEditor( final Path path ) { | ||
| + final FileEditorTab tab = new FileEditorTab( path ); | ||
| + | ||
| + tab.setOnCloseRequest( e -> { | ||
| + if( !canCloseEditor( tab ) ) { | ||
| + e.consume(); | ||
| + } | ||
| + } ); | ||
| + | ||
| + return tab; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Called when the user selects New from the File menu. | ||
| + * | ||
| + * @return The newly added tab. | ||
| + */ | ||
| + void newEditor() { | ||
| + final FileEditorTab tab = createFileEditor( null ); | ||
| + | ||
| + getTabs().add( tab ); | ||
| + getSelectionModel().select( tab ); | ||
| + } | ||
| + | ||
| + void openFileDialog() { | ||
| + final String title = get( "Dialog.file.choose.open.title" ); | ||
| + final FileChooser dialog = createFileChooser( title ); | ||
| + final List<File> files = dialog.showOpenMultipleDialog( getWindow() ); | ||
| + | ||
| + if( files != null ) { | ||
| + openFiles( files ); | ||
| + } | ||
| + } | ||
| + | ||
| + /** | ||
| + * Opens the files into new editors, unless one of those files was a | ||
| + * definition file. The definition file is loaded into the definition pane, | ||
| + * but only the first one selected (multiple definition files will result in a | ||
| + * warning). | ||
| + * | ||
| + * @param files The list of non-definition files that the were requested to | ||
| + * open. | ||
| + * | ||
| + * @return A list of files that can be opened in text editors. | ||
| + */ | ||
| + private void openFiles( final List<File> files ) { | ||
| + final FileTypePredicate predicate | ||
| + = new FileTypePredicate( createExtensionFilter( DEFINITION ).getExtensions() ); | ||
| + | ||
| + // The user might have opened multiple definitions files. These will | ||
| + // be discarded from the text editable files. | ||
| + final List<File> definitions | ||
| + = files.stream().filter( predicate ).collect( Collectors.toList() ); | ||
| + | ||
| + // Create a modifiable list to remove any definition files that were | ||
| + // opened. | ||
| + final List<File> editors = new ArrayList<>( files ); | ||
| + | ||
| + if( editors.size() > 0 ) { | ||
| + saveLastDirectory( editors.get( 0 ) ); | ||
| + } | ||
| + | ||
| + editors.removeAll( definitions ); | ||
| + | ||
| + // Open editor-friendly files (e.g,. Markdown, XML) in new tabs. | ||
| + if( editors.size() > 0 ) { | ||
| + openEditors( editors, 0 ); | ||
| + } | ||
| + | ||
| + if( definitions.size() > 0 ) { | ||
| + openDefinition( definitions.get( 0 ) ); | ||
| + } | ||
| + } | ||
| + | ||
| + private void openEditors( final List<File> files, final int activeIndex ) { | ||
| + final int fileTally = files.size(); | ||
| + final List<Tab> tabs = getTabs(); | ||
| + | ||
| + // Close single unmodified "Untitled" tab. | ||
| + if( tabs.size() == 1 ) { | ||
| + final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 )); | ||
| + | ||
| + if( fileEditor.getPath() == null && !fileEditor.isModified() ) { | ||
| + closeEditor( fileEditor, false ); | ||
| + } | ||
| + } | ||
| + | ||
| + for( int i = 0; i < fileTally; i++ ) { | ||
| + final Path path = files.get( i ).toPath(); | ||
| + | ||
| + FileEditorTab fileEditorTab = findEditor( path ); | ||
| + | ||
| + // Only open new files. | ||
| + if( fileEditorTab == null ) { | ||
| + fileEditorTab = createFileEditor( path ); | ||
| + getTabs().add( fileEditorTab ); | ||
| + } | ||
| + | ||
| + // Select the first file in the list. | ||
| + if( i == activeIndex ) { | ||
| + getSelectionModel().select( fileEditorTab ); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns a property that changes when a new definition file is opened. | ||
| + * | ||
| + * @return The path to a definition file that was opened. | ||
| + */ | ||
| + public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() { | ||
| + return getOnOpenDefinitionFile().getReadOnlyProperty(); | ||
| + } | ||
| + | ||
| + private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() { | ||
| + return this.openDefinition; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Called when the user has opened a definition file (using the file open | ||
| + * dialog box). This will replace the current set of definitions for the | ||
| + * active tab. | ||
| + * | ||
| + * @param definition The file to open. | ||
| + */ | ||
| + private void openDefinition( final File definition ) { | ||
| + // TODO: Prevent reading this file twice when a new text document is opened. | ||
| + // (might be a matter of checking the value first). | ||
| + getOnOpenDefinitionFile().set( definition.toPath() ); | ||
| + } | ||
| + | ||
| + 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; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Answers whether the file has had modifications. ' | ||
| + * | ||
| + * @param tab THe tab to check for modifications. | ||
| + * | ||
| + * @return false The file is unmodified. | ||
| + */ | ||
| + boolean canCloseEditor( final FileEditorTab tab ) { | ||
| + if( !tab.isModified() ) { | ||
| + return true; | ||
| + } | ||
| + | ||
| + final Notification message = getNotifyService().createNotification( | ||
| + Messages.get( "Alert.file.close.title" ), | ||
| + Messages.get( "Alert.file.close.text" ), | ||
| + tab.getText() | ||
| + ); | ||
| + | ||
| + final Alert alert = getNotifyService().createConfirmation( | ||
| + getWindow(), message ); | ||
| + final ButtonType response = alert.showAndWait().get(); | ||
| + | ||
| + return response == YES ? saveEditor( tab ) : response == NO; | ||
| + } | ||
| + | ||
| + private Notifier getNotifyService() { | ||
| + return this.notifyService; | ||
| + } | ||
| + | ||
| + boolean closeEditor( FileEditorTab fileEditor, boolean save ) { | ||
| + if( fileEditor == null ) { | ||
| + return true; | ||
| + } | ||
| + | ||
| + final Tab tab = fileEditor; | ||
| + | ||
| + if( save ) { | ||
| + Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT ); | ||
| + Event.fireEvent( tab, event ); | ||
| + | ||
| + if( event.isConsumed() ) { | ||
| + return false; | ||
| + } | ||
| + } | ||
| + | ||
| + getTabs().remove( tab ); | ||
| + | ||
| + if( tab.getOnClosed() != null ) { | ||
| + Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) ); | ||
| + } | ||
| + | ||
| + return true; | ||
| + } | ||
| + | ||
| + boolean closeAllEditors() { | ||
| + final FileEditorTab[] allEditors = getAllEditors(); | ||
| + final FileEditorTab activeEditor = getActiveFileEditor(); | ||
| + | ||
| + // try to save active tab first because in case the user decides to cancel, | ||
| + // then it stays active | ||
| + if( activeEditor != null && !canCloseEditor( activeEditor ) ) { | ||
| + return false; | ||
| + } | ||
| + | ||
| + // This should be called any time a tab changes. | ||
| + persistPreferences(); | ||
| + | ||
| + // save modified tabs | ||
| + for( int i = 0; i < allEditors.length; i++ ) { | ||
| + final FileEditorTab fileEditor = allEditors[ i ]; | ||
| + | ||
| + if( fileEditor == activeEditor ) { | ||
| + continue; | ||
| + } | ||
| + | ||
| + if( fileEditor.isModified() ) { | ||
| + // activate the modified tab to make its modified content visible to the user | ||
| + getSelectionModel().select( i ); | ||
| + | ||
| + if( !canCloseEditor( fileEditor ) ) { | ||
| + return false; | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + // Close all tabs. | ||
| + for( final FileEditorTab fileEditor : allEditors ) { | ||
| + if( !closeEditor( fileEditor, false ) ) { | ||
| + return false; | ||
| + } | ||
| + } | ||
| + | ||
| + return getTabs().isEmpty(); | ||
| + } | ||
| + | ||
| + private FileEditorTab[] getAllEditors() { | ||
| + final ObservableList<Tab> tabs = getTabs(); | ||
| + final int length = tabs.size(); | ||
| + final FileEditorTab[] allEditors = new FileEditorTab[ length ]; | ||
| + | ||
| + for( int i = 0; i < length; i++ ) { | ||
| + allEditors[ i ] = (FileEditorTab)tabs.get( i ); | ||
| + } | ||
| + | ||
| + return allEditors; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the file editor tab that has the given path. | ||
| + * | ||
| + * @return null No file editor tab for the given path was found. | ||
| + */ | ||
| + private FileEditorTab findEditor( final Path path ) { | ||
| + for( final Tab tab : getTabs() ) { | ||
| + final FileEditorTab fileEditor = (FileEditorTab)tab; | ||
| + | ||
| + if( fileEditor.isPath( path ) ) { | ||
| + return fileEditor; | ||
| + } | ||
| + } | ||
| + | ||
| + return null; | ||
| + } | ||
| + | ||
| + private FileChooser createFileChooser( String title ) { | ||
| + final FileChooser fileChooser = new FileChooser(); | ||
| + | ||
| + fileChooser.setTitle( title ); | ||
| + fileChooser.getExtensionFilters().addAll( | ||
| + createExtensionFilters() ); | ||
| + | ||
| + final String lastDirectory = getPreferences().get( "lastDirectory", null ); | ||
| + File file = new File( (lastDirectory != null) ? lastDirectory : "." ); | ||
| + | ||
| + if( !file.isDirectory() ) { | ||
| + file = new File( "." ); | ||
| + } | ||
| + | ||
| + fileChooser.setInitialDirectory( file ); | ||
| + return fileChooser; | ||
| + } | ||
| + | ||
| + private List<ExtensionFilter> createExtensionFilters() { | ||
| + final List<ExtensionFilter> list = new ArrayList<>(); | ||
| + | ||
| + // TODO: Return a list of all properties that match the filter prefix. | ||
| + // This will allow dynamic filters to be added and removed just by | ||
| + // updating the properties file. | ||
| + list.add( createExtensionFilter( MARKDOWN ) ); | ||
| + list.add( createExtensionFilter( DEFINITION ) ); | ||
| + list.add( createExtensionFilter( XML ) ); | ||
| + list.add( createExtensionFilter( ALL ) ); | ||
| + return list; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns a filter for file name extensions recognized by the application | ||
| + * that can be opened by the user. | ||
| + * | ||
| + * @param filetype Used to find the globbing pattern for extensions. | ||
| + * | ||
| + * @return A filename filter suitable for use by a FileDialog instance. | ||
| + */ | ||
| + private ExtensionFilter createExtensionFilter( final FileType filetype ) { | ||
| + final String tKey = String.format( "%s.title.%s", FILTER_EXTENSION_TITLES, filetype ); | ||
| + final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype ); | ||
| return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) ); |
| import javafx.scene.image.Image; | ||
| import javafx.stage.Stage; | ||
| -import com.scrivenvar.service.events.NotifyService; | ||
| +import com.scrivenvar.service.events.Notifier; | ||
| /** | ||
| public void start( final Stage stage ) throws Exception { | ||
| initApplication(); | ||
| + initNotifyService(); | ||
| initState( stage ); | ||
| initStage( stage ); | ||
| - initAlertService(); | ||
| - initWatchDog(); | ||
| + initSnitch(); | ||
| stage.show(); | ||
| private void initApplication() { | ||
| app = this; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Constructs the notify service and appends the main window to the list of | ||
| + * notification observers. | ||
| + */ | ||
| + private void initNotifyService() { | ||
| + final Notifier service = Services.load(Notifier.class ); | ||
| + service.addObserver( getMainWindow() ); | ||
| } | ||
| stage.setTitle( getApplicationTitle() ); | ||
| stage.setScene( getScene() ); | ||
| - } | ||
| - | ||
| - private void initAlertService() { | ||
| - final NotifyService service = Services.load(NotifyService.class ); | ||
| - service.setWindow( getScene().getWindow() ); | ||
| } | ||
| - private void initWatchDog() { | ||
| - setSnitchThread( new Thread( getWatchDog() ) ); | ||
| + private void initSnitch() { | ||
| + setSnitchThread( new Thread( getSnitch() ) ); | ||
| getSnitchThread().start(); | ||
| } | ||
| @Override | ||
| public void stop() throws InterruptedException { | ||
| - getWatchDog().stop(); | ||
| + getSnitch().stop(); | ||
| final Thread thread = getSnitchThread(); | ||
| if( thread != null ) { | ||
| thread.interrupt(); | ||
| thread.join(); | ||
| } | ||
| } | ||
| - private synchronized Snitch getWatchDog() { | ||
| + private synchronized Snitch getSnitch() { | ||
| if( this.snitch == null ) { | ||
| this.snitch = Services.load( Snitch.class ); | ||
| } | ||
| - | ||
| + | ||
| return this.snitch; | ||
| } | ||
| import com.scrivenvar.service.Options; | ||
| import com.scrivenvar.service.Snitch; | ||
| -import com.scrivenvar.util.Action; | ||
| -import com.scrivenvar.util.ActionUtils; | ||
| -import static com.scrivenvar.util.StageState.*; | ||
| -import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*; | ||
| -import java.nio.file.Path; | ||
| -import java.util.HashMap; | ||
| -import java.util.Map; | ||
| -import java.util.Observable; | ||
| -import java.util.Observer; | ||
| -import java.util.function.Function; | ||
| -import java.util.prefs.Preferences; | ||
| -import javafx.application.Platform; | ||
| -import javafx.beans.binding.Bindings; | ||
| -import javafx.beans.binding.BooleanBinding; | ||
| -import javafx.beans.property.BooleanProperty; | ||
| -import javafx.beans.property.SimpleBooleanProperty; | ||
| -import javafx.beans.value.ObservableBooleanValue; | ||
| -import javafx.beans.value.ObservableValue; | ||
| -import javafx.collections.ListChangeListener.Change; | ||
| -import javafx.collections.ObservableList; | ||
| -import static javafx.event.Event.fireEvent; | ||
| -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 static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST; | ||
| - | ||
| -/** | ||
| - * Main window containing a tab pane in the center for file editors. | ||
| - * | ||
| - * @author Karl Tauber and White Magic Software, Ltd. | ||
| - */ | ||
| -public class MainWindow implements Observer { | ||
| - | ||
| - private final Options options = Services.load( Options.class ); | ||
| - private final Snitch snitch = Services.load( Snitch.class ); | ||
| - | ||
| - private Scene scene; | ||
| - private MenuBar menuBar; | ||
| - | ||
| - private DefinitionSource definitionSource; | ||
| - private DefinitionPane definitionPane; | ||
| - private FileEditorTabPane fileEditorPane; | ||
| - private HTMLPreviewPane previewPane; | ||
| - | ||
| - /** | ||
| - * Prevent re-instantiation processing classes. | ||
| - */ | ||
| - private Map<FileEditorTab, Processor<String>> processors; | ||
| - | ||
| - public MainWindow() { | ||
| - initLayout(); | ||
| - initDefinitionListener(); | ||
| - initTabAddedListener(); | ||
| - initTabChangedListener(); | ||
| - initPreferences(); | ||
| - initWatchDog(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Listen for file editor tab pane to receive an open definition source event. | ||
| - */ | ||
| - private void initDefinitionListener() { | ||
| - getFileEditorPane().onOpenDefinitionFileProperty().addListener( | ||
| - (ObservableValue<? extends Path> definitionFile, | ||
| - final Path oldPath, final Path newPath) -> { | ||
| - openDefinition( newPath ); | ||
| - | ||
| - // Indirectly refresh the resolved map. | ||
| - setProcessors( null ); | ||
| - | ||
| - // Will create new processors and therefore a new resolved map. | ||
| - refreshSelectedTab( getActiveFileEditor() ); | ||
| - | ||
| - updateDefinitionPane(); | ||
| - } | ||
| - ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * When tabs are added, hook the various change listeners onto the new tab so | ||
| - * that the preview pane refreshes as necessary. | ||
| - */ | ||
| - private void initTabAddedListener() { | ||
| - final FileEditorTabPane editorPane = getFileEditorPane(); | ||
| - | ||
| - // Make sure the text processor kicks off when new files are opened. | ||
| - final ObservableList<Tab> tabs = editorPane.getTabs(); | ||
| - | ||
| - // Update the preview pane on tab changes. | ||
| - tabs.addListener( | ||
| - (final Change<? extends Tab> change) -> { | ||
| - while( change.next() ) { | ||
| - if( change.wasAdded() ) { | ||
| - // Multiple tabs can be added simultaneously. | ||
| - for( final Tab newTab : change.getAddedSubList() ) { | ||
| - final FileEditorTab tab = (FileEditorTab)newTab; | ||
| - | ||
| - initTextChangeListener( tab ); | ||
| - initCaretParagraphListener( tab ); | ||
| - initVariableNameInjector( tab ); | ||
| - } | ||
| - } | ||
| - } | ||
| - } | ||
| - ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Reloads the preferences from the previous load. | ||
| - */ | ||
| - private void initPreferences() { | ||
| - restoreDefinitionSource(); | ||
| - getFileEditorPane().restorePreferences(); | ||
| - updateDefinitionPane(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Listen for new tab selection events. | ||
| - */ | ||
| - private void initTabChangedListener() { | ||
| - final FileEditorTabPane editorPane = getFileEditorPane(); | ||
| - | ||
| - // Update the preview pane changing tabs. | ||
| - editorPane.addTabSelectionListener( | ||
| - (ObservableValue<? extends Tab> tabPane, | ||
| - final Tab oldTab, final Tab newTab) -> { | ||
| - | ||
| - // If there was no old tab, then this is a first time load, which | ||
| - // can be ignored. | ||
| - if( oldTab != null ) { | ||
| - if( newTab == null ) { | ||
| - closeRemainingTab(); | ||
| - } else { | ||
| - // Update the preview with the edited text. | ||
| - refreshSelectedTab( (FileEditorTab)newTab ); | ||
| - } | ||
| - } | ||
| - } | ||
| - ); | ||
| - } | ||
| - | ||
| - private void initTextChangeListener( final FileEditorTab tab ) { | ||
| - tab.addTextChangeListener( | ||
| - (ObservableValue<? extends String> editor, | ||
| - final String oldValue, final String newValue) -> { | ||
| - refreshSelectedTab( tab ); | ||
| - } | ||
| - ); | ||
| - } | ||
| - | ||
| - private void initCaretParagraphListener( final FileEditorTab tab ) { | ||
| - tab.addCaretParagraphListener( | ||
| - (ObservableValue<? extends Integer> editor, | ||
| - final Integer oldValue, final Integer newValue) -> { | ||
| - refreshSelectedTab( tab ); | ||
| - } | ||
| - ); | ||
| - } | ||
| - | ||
| - private void initVariableNameInjector( final FileEditorTab tab ) { | ||
| - VariableNameInjector.listen( tab, getDefinitionPane() ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Watch for changes to external files. In particular, this awaits | ||
| - * modifications to any XSL files associated with XML files being edited. When | ||
| - * an XSL file is modified (external to the application), the watchdog's ears | ||
| - * perk up and the file is reloaded. This keeps the XSL transformation up to | ||
| - * date with what's on the file system. | ||
| - */ | ||
| - private void initWatchDog() { | ||
| - getSnitch().addObserver( this ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Called whenever the preview pane becomes out of sync with the file editor | ||
| - * tab. This can be called when the text changes, the caret paragraph changes, | ||
| - * or the file tab changes. | ||
| - * | ||
| - * @param tab The file editor tab that has been changed in some fashion. | ||
| - */ | ||
| - private void refreshSelectedTab( final FileEditorTab tab ) { | ||
| - if( tab.isFileOpen() ) { | ||
| - getPreviewPane().setPath( tab.getPath() ); | ||
| - | ||
| - Processor<String> processor = getProcessors().get( tab ); | ||
| - | ||
| - if( processor == null ) { | ||
| - processor = createProcessor( tab ); | ||
| - getProcessors().put( tab, processor ); | ||
| - } | ||
| - | ||
| - processor.processChain( tab.getEditorText() ); | ||
| - } | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the variable map of interpolated definitions. | ||
| - * | ||
| - * @return A map to help dereference variables. | ||
| - */ | ||
| - private Map<String, String> getResolvedMap() { | ||
| - return getDefinitionSource().getResolvedMap(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the root node for the hierarchical definition source. | ||
| - * | ||
| - * @return Data to display in the definition pane. | ||
| - */ | ||
| - private TreeView<String> getTreeView() { | ||
| - try { | ||
| - return getDefinitionSource().asTreeView(); | ||
| - } catch( Exception e ) { | ||
| - alert( e ); | ||
| - } | ||
| - | ||
| - return new TreeView<>(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Called when a definition source is opened. | ||
| - * | ||
| - * @param path Path to the definition source that was opened. | ||
| - */ | ||
| - private void openDefinition( final Path path ) { | ||
| - try { | ||
| - final DefinitionSource ds = createDefinitionSource( path.toString() ); | ||
| - setDefinitionSource( ds ); | ||
| - storeDefinitionSource(); | ||
| - updateDefinitionPane(); | ||
| - } catch( final Exception e ) { | ||
| - alert( e ); | ||
| - } | ||
| - } | ||
| - | ||
| - private void updateDefinitionPane() { | ||
| - getDefinitionPane().setRoot( getDefinitionSource().asTreeView() ); | ||
| - } | ||
| - | ||
| - private void restoreDefinitionSource() { | ||
| - final Preferences preferences = getPreferences(); | ||
| - final String source = preferences.get( PREFS_DEFINITION_SOURCE, null ); | ||
| - | ||
| - // If there's no definition source set, don't try to load it. | ||
| - if( source != null ) { | ||
| - setDefinitionSource( createDefinitionSource( source ) ); | ||
| - } | ||
| - } | ||
| - | ||
| - private void storeDefinitionSource() { | ||
| - final Preferences preferences = getPreferences(); | ||
| - final DefinitionSource ds = getDefinitionSource(); | ||
| - | ||
| - preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Called when the last open tab is closed to clear the preview pane. | ||
| - */ | ||
| - private void closeRemainingTab() { | ||
| - getPreviewPane().clear(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Called when an exception occurs that warrants the user's attention. | ||
| - * | ||
| - * @param e The exception with a message that the user should know about. | ||
| - */ | ||
| - private void alert( final Exception e ) { | ||
| - // TODO: Update the status bar or do something clever with the error. | ||
| - } | ||
| - | ||
| - //---- File actions ------------------------------------------------------- | ||
| - /** | ||
| - * Called when a file has been modified. | ||
| - * | ||
| - * @param snitch The watchdog file monitoring instance. | ||
| - * @param file The file that was modified. | ||
| - */ | ||
| - @Override | ||
| - public void update( final Observable snitch, final Object file ) { | ||
| - if( file instanceof Path ) { | ||
| - update( (Path)file ); | ||
| - } | ||
| - } | ||
| - | ||
| - /** | ||
| - * Called when a file has been modified. | ||
| - * | ||
| - * @param file Path to the modified file. | ||
| - */ | ||
| - private void update( final Path file ) { | ||
| - // Avoid throwing IllegalStateException by running from a non-JavaFX thread. | ||
| - Platform.runLater( | ||
| - () -> { | ||
| - // Brute-force XSLT file reload by re-instantiating all processors. | ||
| - resetProcessors(); | ||
| - refreshSelectedTab( getActiveFileEditor() ); | ||
| - } | ||
| - ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * After resetting the processors, they will refresh anew to be up-to-date | ||
| - * with the files (text and definition) currently loaded into the editor. | ||
| - */ | ||
| - private void resetProcessors() { | ||
| - getProcessors().clear(); | ||
| - } | ||
| - | ||
| - //---- File actions ------------------------------------------------------- | ||
| - private void fileNew() { | ||
| - getFileEditorPane().newEditor(); | ||
| - } | ||
| - | ||
| - private void fileOpen() { | ||
| - getFileEditorPane().openFileDialog(); | ||
| - } | ||
| - | ||
| - private void fileClose() { | ||
| - getFileEditorPane().closeEditor( getActiveFileEditor(), true ); | ||
| - } | ||
| - | ||
| - private void fileCloseAll() { | ||
| - getFileEditorPane().closeAllEditors(); | ||
| - } | ||
| - | ||
| - private void fileSave() { | ||
| - getFileEditorPane().saveEditor( getActiveFileEditor() ); | ||
| - } | ||
| - | ||
| - private void fileSaveAll() { | ||
| - getFileEditorPane().saveAllEditors(); | ||
| - } | ||
| - | ||
| - private void fileExit() { | ||
| - final Window window = getWindow(); | ||
| - fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) ); | ||
| - } | ||
| - | ||
| - //---- Help actions ------------------------------------------------------- | ||
| - private void helpAbout() { | ||
| - Alert alert = new Alert( AlertType.INFORMATION ); | ||
| - alert.setTitle( get( "Dialog.about.title" ) ); | ||
| - alert.setHeaderText( get( "Dialog.about.header" ) ); | ||
| - alert.setContentText( get( "Dialog.about.content" ) ); | ||
| - alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) ); | ||
| - alert.initOwner( getWindow() ); | ||
| - | ||
| - alert.showAndWait(); | ||
| - } | ||
| - | ||
| - //---- Convenience accessors ---------------------------------------------- | ||
| - private float getFloat( final String key, final float defaultValue ) { | ||
| - return getPreferences().getFloat( key, defaultValue ); | ||
| - } | ||
| - | ||
| - private Preferences getPreferences() { | ||
| - return getOptions().getState(); | ||
| - } | ||
| - | ||
| - private Window getWindow() { | ||
| - return getScene().getWindow(); | ||
| - } | ||
| - | ||
| - private MarkdownEditorPane getActiveEditor() { | ||
| - final EditorPane pane = getActiveFileEditor().getEditorPane(); | ||
| - | ||
| - return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null; | ||
| - } | ||
| - | ||
| - private FileEditorTab getActiveFileEditor() { | ||
| - return getFileEditorPane().getActiveFileEditor(); | ||
| - } | ||
| - | ||
| - //---- Member accessors --------------------------------------------------- | ||
| - private void setScene( Scene scene ) { | ||
| - this.scene = scene; | ||
| - } | ||
| - | ||
| - public Scene getScene() { | ||
| - return this.scene; | ||
| - } | ||
| - | ||
| - private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) { | ||
| - this.processors = map; | ||
| - } | ||
| - | ||
| - private Map<FileEditorTab, Processor<String>> getProcessors() { | ||
| - if( this.processors == null ) { | ||
| - setProcessors( new HashMap<>() ); | ||
| - } | ||
| - | ||
| - return this.processors; | ||
| - } | ||
| - | ||
| - private FileEditorTabPane getFileEditorPane() { | ||
| - if( this.fileEditorPane == null ) { | ||
| - this.fileEditorPane = createFileEditorPane(); | ||
| - } | ||
| - | ||
| - return this.fileEditorPane; | ||
| - } | ||
| - | ||
| - private HTMLPreviewPane getPreviewPane() { | ||
| - if( this.previewPane == null ) { | ||
| - this.previewPane = createPreviewPane(); | ||
| - } | ||
| - | ||
| - return this.previewPane; | ||
| - } | ||
| - | ||
| - private void setDefinitionSource( final DefinitionSource definitionSource ) { | ||
| - this.definitionSource = definitionSource; | ||
| - } | ||
| - | ||
| - private DefinitionSource getDefinitionSource() { | ||
| - if( this.definitionSource == null ) { | ||
| - this.definitionSource = new EmptyDefinitionSource(); | ||
| - } | ||
| - | ||
| - return this.definitionSource; | ||
| - } | ||
| - | ||
| - private DefinitionPane getDefinitionPane() { | ||
| - if( this.definitionPane == null ) { | ||
| - this.definitionPane = createDefinitionPane(); | ||
| - } | ||
| - | ||
| - return this.definitionPane; | ||
| - } | ||
| - | ||
| - private Options getOptions() { | ||
| - return this.options; | ||
| - } | ||
| - | ||
| - private Snitch getSnitch() { | ||
| - return this.snitch; | ||
| - } | ||
| - | ||
| - public void setMenuBar( MenuBar menuBar ) { | ||
| - this.menuBar = menuBar; | ||
| - } | ||
| - | ||
| - public MenuBar getMenuBar() { | ||
| - return this.menuBar; | ||
| - } | ||
| - | ||
| - //---- Member creators ---------------------------------------------------- | ||
| - /** | ||
| - * Factory to create processors that are suited to different file types. | ||
| - * | ||
| - * @param tab The tab that is subjected to processing. | ||
| - * | ||
| - * @return A processor suited to the file type specified by the tab's path. | ||
| - */ | ||
| - private Processor<String> createProcessor( final FileEditorTab tab ) { | ||
| - return createProcessorFactory().createProcessor( tab ); | ||
| - } | ||
| - | ||
| - private ProcessorFactory createProcessorFactory() { | ||
| - return new ProcessorFactory( getPreviewPane(), getResolvedMap() ); | ||
| - } | ||
| - | ||
| - private DefinitionSource createDefinitionSource( final String path ) { | ||
| - return createDefinitionFactory().createDefinitionSource( path ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Create an editor pane to hold file editor tabs. | ||
| - * | ||
| - * @return A new instance, never null. | ||
| - */ | ||
| - private FileEditorTabPane createFileEditorPane() { | ||
| - return new FileEditorTabPane(); | ||
| - } | ||
| - | ||
| - private HTMLPreviewPane createPreviewPane() { | ||
| - return new HTMLPreviewPane(); | ||
| - } | ||
| - | ||
| - private DefinitionPane createDefinitionPane() { | ||
| - return new DefinitionPane( getTreeView() ); | ||
| - } | ||
| - | ||
| - private DefinitionFactory createDefinitionFactory() { | ||
| - return new DefinitionFactory(); | ||
| - } | ||
| - | ||
| - private Node createMenuBar() { | ||
| - final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull(); | ||
| - | ||
| - // File actions | ||
| - Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() ); | ||
| - Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() ); | ||
| - Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull ); | ||
| - Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull ); | ||
| - Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(), | ||
| - createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() ); | ||
| - Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(), | ||
| - Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) ); | ||
| - Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() ); | ||
| - | ||
| - // Edit actions | ||
| - Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO, | ||
| - e -> getActiveEditor().undo(), | ||
| - createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() ); | ||
| - Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT, | ||
| - e -> getActiveEditor().redo(), | ||
| - createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() ); | ||
| - | ||
| - // Insert actions | ||
| - Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD, | ||
| - e -> getActiveEditor().surroundSelection( "**", "**" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC, | ||
| - e -> getActiveEditor().surroundSelection( "*", "*" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertSuperscriptAction = new Action( get( "Main.menu.insert.superscript" ), "Shortcut+[", SUPERSCRIPT, | ||
| - e -> getActiveEditor().surroundSelection( "^", "^" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertSubscriptAction = new Action( get( "Main.menu.insert.subscript" ), "Shortcut+]", SUBSCRIPT, | ||
| - e -> getActiveEditor().surroundSelection( "~", "~" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH, | ||
| - e -> getActiveEditor().surroundSelection( "~~", "~~" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac | ||
| - e -> getActiveEditor().surroundSelection( "\n\n> ", "" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE, | ||
| - e -> getActiveEditor().surroundSelection( "`", "`" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT, | ||
| - e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ), | ||
| - activeFileEditorIsNull ); | ||
| - | ||
| - Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK, | ||
| - e -> getActiveEditor().insertLink(), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT, | ||
| - e -> getActiveEditor().insertImage(), | ||
| - activeFileEditorIsNull ); | ||
| - | ||
| - final Action[] headers = new Action[ 6 ]; | ||
| - | ||
| - // Insert header actions (H1 ... H6) | ||
| - for( int i = 1; i <= 6; i++ ) { | ||
| - final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | ||
| - final String markup = String.format( "%n%n%s ", hashes ); | ||
| - final String text = get( "Main.menu.insert.header_" + i ); | ||
| - final String accelerator = "Shortcut+" + i; | ||
| - final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" ); | ||
| - | ||
| - headers[ i - 1 ] = new Action( text, accelerator, HEADER, | ||
| - e -> getActiveEditor().surroundSelection( markup, "", prompt ), | ||
| - activeFileEditorIsNull ); | ||
| - } | ||
| - | ||
| - Action insertUnorderedListAction = new Action( get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL, | ||
| - e -> getActiveEditor().surroundSelection( "\n\n* ", "" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertOrderedListAction = new Action( get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL, | ||
| - e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertHorizontalRuleAction = new Action( get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null, | ||
| - e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ), | ||
| - activeFileEditorIsNull ); | ||
| - | ||
| - // Help actions | ||
| - Action helpAboutAction = new Action( get( "Main.menu.help.about" ), null, null, e -> helpAbout() ); | ||
| - | ||
| - //---- MenuBar ---- | ||
| - Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ), | ||
| - fileNewAction, | ||
| - fileOpenAction, | ||
| - null, | ||
| - fileCloseAction, | ||
| - fileCloseAllAction, | ||
| - null, | ||
| - fileSaveAction, | ||
| - fileSaveAllAction, | ||
| - null, | ||
| - fileExitAction ); | ||
| - | ||
| - Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ), | ||
| - editUndoAction, | ||
| - editRedoAction ); | ||
| - | ||
| - Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ), | ||
| - insertBoldAction, | ||
| - insertItalicAction, | ||
| - insertSuperscriptAction, | ||
| - insertSubscriptAction, | ||
| - insertStrikethroughAction, | ||
| - insertBlockquoteAction, | ||
| - insertCodeAction, | ||
| - insertFencedCodeBlockAction, | ||
| - null, | ||
| - insertLinkAction, | ||
| - insertImageAction, | ||
| - null, | ||
| - headers[ 0 ], | ||
| - headers[ 1 ], | ||
| - headers[ 2 ], | ||
| - headers[ 3 ], | ||
| - headers[ 4 ], | ||
| - headers[ 5 ], | ||
| - null, | ||
| - insertUnorderedListAction, | ||
| - insertOrderedListAction, | ||
| - insertHorizontalRuleAction ); | ||
| - | ||
| - Menu helpMenu = ActionUtils.createMenu( 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, | ||
| - insertSuperscriptAction, | ||
| - insertSubscriptAction, | ||
| - insertBlockquoteAction, | ||
| - insertCodeAction, | ||
| - insertFencedCodeBlockAction, | ||
| - null, | ||
| - insertLinkAction, | ||
| - insertImageAction, | ||
| - null, | ||
| - headers[ 0 ], | ||
| - null, | ||
| - insertUnorderedListAction, | ||
| - insertOrderedListAction ); | ||
| - | ||
| - return new VBox( menuBar, toolBar ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Creates a boolean property that is bound to another boolean value of the | ||
| - * active editor. | ||
| - */ | ||
| - private BooleanProperty createActiveBooleanProperty( | ||
| - final Function<FileEditorTab, ObservableBooleanValue> func ) { | ||
| - | ||
| - final BooleanProperty b = new SimpleBooleanProperty(); | ||
| - final FileEditorTab tab = getActiveFileEditor(); | ||
| - | ||
| - if( tab != null ) { | ||
| - b.bind( func.apply( tab ) ); | ||
| - } | ||
| - | ||
| - getFileEditorPane().activeFileEditorProperty().addListener( | ||
| - (observable, oldFileEditor, newFileEditor) -> { | ||
| - b.unbind(); | ||
| - | ||
| - if( newFileEditor != null ) { | ||
| - b.bind( func.apply( newFileEditor ) ); | ||
| - } else { | ||
| - b.set( false ); | ||
| - } | ||
| - } | ||
| - ); | ||
| - | ||
| - return b; | ||
| - } | ||
| - | ||
| - private void initLayout() { | ||
| - final 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() ); | ||
| +import com.scrivenvar.service.events.Notifier; | ||
| +import com.scrivenvar.util.Action; | ||
| +import com.scrivenvar.util.ActionUtils; | ||
| +import static com.scrivenvar.util.StageState.*; | ||
| +import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*; | ||
| +import java.nio.file.Path; | ||
| +import java.util.HashMap; | ||
| +import java.util.Map; | ||
| +import java.util.Observable; | ||
| +import java.util.Observer; | ||
| +import java.util.function.Function; | ||
| +import java.util.prefs.Preferences; | ||
| +import javafx.application.Platform; | ||
| +import javafx.beans.binding.Bindings; | ||
| +import javafx.beans.binding.BooleanBinding; | ||
| +import javafx.beans.property.BooleanProperty; | ||
| +import javafx.beans.property.SimpleBooleanProperty; | ||
| +import javafx.beans.value.ObservableBooleanValue; | ||
| +import javafx.beans.value.ObservableValue; | ||
| +import javafx.collections.ListChangeListener.Change; | ||
| +import javafx.collections.ObservableList; | ||
| +import static javafx.event.Event.fireEvent; | ||
| +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 static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST; | ||
| +import org.controlsfx.control.StatusBar; | ||
| + | ||
| +/** | ||
| + * Main window containing a tab pane in the center for file editors. | ||
| + * | ||
| + * @author Karl Tauber and White Magic Software, Ltd. | ||
| + */ | ||
| +public class MainWindow implements Observer { | ||
| + | ||
| + private final Options options = Services.load( Options.class ); | ||
| + private final Snitch snitch = Services.load( Snitch.class ); | ||
| + private final Notifier notifier = Services.load( Notifier.class ); | ||
| + | ||
| + private Scene scene; | ||
| + private MenuBar menuBar; | ||
| + private StatusBar statusBar; | ||
| + | ||
| + private DefinitionSource definitionSource; | ||
| + private DefinitionPane definitionPane; | ||
| + private FileEditorTabPane fileEditorPane; | ||
| + private HTMLPreviewPane previewPane; | ||
| + | ||
| + /** | ||
| + * Prevent re-instantiation processing classes. | ||
| + */ | ||
| + private Map<FileEditorTab, Processor<String>> processors; | ||
| + | ||
| + public MainWindow() { | ||
| + initLayout(); | ||
| + initDefinitionListener(); | ||
| + initTabAddedListener(); | ||
| + initTabChangedListener(); | ||
| + initPreferences(); | ||
| + initSnitch(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Listen for file editor tab pane to receive an open definition source event. | ||
| + */ | ||
| + private void initDefinitionListener() { | ||
| + getFileEditorPane().onOpenDefinitionFileProperty().addListener( | ||
| + (ObservableValue<? extends Path> definitionFile, | ||
| + final Path oldPath, final Path newPath) -> { | ||
| + openDefinition( newPath ); | ||
| + | ||
| + // Indirectly refresh the resolved map. | ||
| + setProcessors( null ); | ||
| + | ||
| + // Will create new processors and therefore a new resolved map. | ||
| + refreshSelectedTab( getActiveFileEditor() ); | ||
| + | ||
| + updateDefinitionPane(); | ||
| + } | ||
| + ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * When tabs are added, hook the various change listeners onto the new tab so | ||
| + * that the preview pane refreshes as necessary. | ||
| + */ | ||
| + private void initTabAddedListener() { | ||
| + final FileEditorTabPane editorPane = getFileEditorPane(); | ||
| + | ||
| + // Make sure the text processor kicks off when new files are opened. | ||
| + final ObservableList<Tab> tabs = editorPane.getTabs(); | ||
| + | ||
| + // Update the preview pane on tab changes. | ||
| + tabs.addListener( | ||
| + (final Change<? extends Tab> change) -> { | ||
| + while( change.next() ) { | ||
| + if( change.wasAdded() ) { | ||
| + // Multiple tabs can be added simultaneously. | ||
| + for( final Tab newTab : change.getAddedSubList() ) { | ||
| + final FileEditorTab tab = (FileEditorTab)newTab; | ||
| + | ||
| + initTextChangeListener( tab ); | ||
| + initCaretParagraphListener( tab ); | ||
| + initVariableNameInjector( tab ); | ||
| + } | ||
| + } | ||
| + } | ||
| + } | ||
| + ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Reloads the preferences from the previous load. | ||
| + */ | ||
| + private void initPreferences() { | ||
| + restoreDefinitionSource(); | ||
| + getFileEditorPane().restorePreferences(); | ||
| + updateDefinitionPane(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Listen for new tab selection events. | ||
| + */ | ||
| + private void initTabChangedListener() { | ||
| + final FileEditorTabPane editorPane = getFileEditorPane(); | ||
| + | ||
| + // Update the preview pane changing tabs. | ||
| + editorPane.addTabSelectionListener( | ||
| + (ObservableValue<? extends Tab> tabPane, | ||
| + final Tab oldTab, final Tab newTab) -> { | ||
| + | ||
| + // If there was no old tab, then this is a first time load, which | ||
| + // can be ignored. | ||
| + if( oldTab != null ) { | ||
| + if( newTab == null ) { | ||
| + closeRemainingTab(); | ||
| + } else { | ||
| + // Update the preview with the edited text. | ||
| + refreshSelectedTab( (FileEditorTab)newTab ); | ||
| + } | ||
| + } | ||
| + } | ||
| + ); | ||
| + } | ||
| + | ||
| + private void initTextChangeListener( final FileEditorTab tab ) { | ||
| + tab.addTextChangeListener( | ||
| + (ObservableValue<? extends String> editor, | ||
| + final String oldValue, final String newValue) -> { | ||
| + refreshSelectedTab( tab ); | ||
| + } | ||
| + ); | ||
| + } | ||
| + | ||
| + private void initCaretParagraphListener( final FileEditorTab tab ) { | ||
| + tab.addCaretParagraphListener( | ||
| + (ObservableValue<? extends Integer> editor, | ||
| + final Integer oldValue, final Integer newValue) -> { | ||
| + refreshSelectedTab( tab ); | ||
| + } | ||
| + ); | ||
| + } | ||
| + | ||
| + private void initVariableNameInjector( final FileEditorTab tab ) { | ||
| + VariableNameInjector.listen( tab, getDefinitionPane() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Watch for changes to external files. In particular, this awaits | ||
| + * modifications to any XSL files associated with XML files being edited. When | ||
| + * an XSL file is modified (external to the application), the snitch's ears | ||
| + * perk up and the file is reloaded. This keeps the XSL transformation up to | ||
| + * date with what's on the file system. | ||
| + */ | ||
| + private void initSnitch() { | ||
| + getSnitch().addObserver( this ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Called whenever the preview pane becomes out of sync with the file editor | ||
| + * tab. This can be called when the text changes, the caret paragraph changes, | ||
| + * or the file tab changes. | ||
| + * | ||
| + * @param tab The file editor tab that has been changed in some fashion. | ||
| + */ | ||
| + private void refreshSelectedTab( final FileEditorTab tab ) { | ||
| + if( tab.isFileOpen() ) { | ||
| + getPreviewPane().setPath( tab.getPath() ); | ||
| + | ||
| + Processor<String> processor = getProcessors().get( tab ); | ||
| + | ||
| + if( processor == null ) { | ||
| + processor = createProcessor( tab ); | ||
| + getProcessors().put( tab, processor ); | ||
| + } | ||
| + | ||
| + try { | ||
| + processor.processChain( tab.getEditorText() ); | ||
| + getNotifier().clear(); | ||
| + } catch( final Exception ex ) { | ||
| + error( ex ); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the variable map of interpolated definitions. | ||
| + * | ||
| + * @return A map to help dereference variables. | ||
| + */ | ||
| + private Map<String, String> getResolvedMap() { | ||
| + return getDefinitionSource().getResolvedMap(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the root node for the hierarchical definition source. | ||
| + * | ||
| + * @return Data to display in the definition pane. | ||
| + */ | ||
| + private TreeView<String> getTreeView() { | ||
| + try { | ||
| + return getDefinitionSource().asTreeView(); | ||
| + } catch( Exception e ) { | ||
| + error( e ); | ||
| + } | ||
| + | ||
| + return new TreeView<>(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Called when a definition source is opened. | ||
| + * | ||
| + * @param path Path to the definition source that was opened. | ||
| + */ | ||
| + private void openDefinition( final Path path ) { | ||
| + try { | ||
| + final DefinitionSource ds = createDefinitionSource( path.toString() ); | ||
| + setDefinitionSource( ds ); | ||
| + storeDefinitionSource(); | ||
| + updateDefinitionPane(); | ||
| + } catch( final Exception e ) { | ||
| + error( e ); | ||
| + } | ||
| + } | ||
| + | ||
| + private void updateDefinitionPane() { | ||
| + getDefinitionPane().setRoot( getDefinitionSource().asTreeView() ); | ||
| + } | ||
| + | ||
| + private void restoreDefinitionSource() { | ||
| + final Preferences preferences = getPreferences(); | ||
| + final String source = preferences.get( PREFS_DEFINITION_SOURCE, null ); | ||
| + | ||
| + // If there's no definition source set, don't try to load it. | ||
| + if( source != null ) { | ||
| + setDefinitionSource( createDefinitionSource( source ) ); | ||
| + } | ||
| + } | ||
| + | ||
| + private void storeDefinitionSource() { | ||
| + final Preferences preferences = getPreferences(); | ||
| + final DefinitionSource ds = getDefinitionSource(); | ||
| + | ||
| + preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Called when the last open tab is closed to clear the preview pane. | ||
| + */ | ||
| + private void closeRemainingTab() { | ||
| + getPreviewPane().clear(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Called when an exception occurs that warrants the user's attention. | ||
| + * | ||
| + * @param e The exception with a message that the user should know about. | ||
| + */ | ||
| + private void error( final Exception e ) { | ||
| + getNotifier().notify( e ); | ||
| + } | ||
| + | ||
| + //---- File actions ------------------------------------------------------- | ||
| + /** | ||
| + * Called when an observable instance has changed. This includes the snitch | ||
| + * service and the notify service. | ||
| + * | ||
| + * @param observable The observed instance. | ||
| + * @param o The noteworthy item. | ||
| + */ | ||
| + @Override | ||
| + public void update( final Observable observable, final Object o ) { | ||
| + if( observable instanceof Snitch ) { | ||
| + if( o instanceof Path ) { | ||
| + update( (Path)o ); | ||
| + } | ||
| + } else if( observable instanceof Notifier && o != null ) { | ||
| + final String s = (String)o; | ||
| + final int index = s.indexOf( '\n' ); | ||
| + final String message = s.substring( 0, index > 0 ? index : s.length() ); | ||
| + | ||
| + getStatusBar().setText( message ); | ||
| + } | ||
| + } | ||
| + | ||
| + /** | ||
| + * Called when a file has been modified. | ||
| + * | ||
| + * @param file Path to the modified file. | ||
| + */ | ||
| + private void update( final Path file ) { | ||
| + // Avoid throwing IllegalStateException by running from a non-JavaFX thread. | ||
| + Platform.runLater( | ||
| + () -> { | ||
| + // Brute-force XSLT file reload by re-instantiating all processors. | ||
| + resetProcessors(); | ||
| + refreshSelectedTab( getActiveFileEditor() ); | ||
| + } | ||
| + ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * After resetting the processors, they will refresh anew to be up-to-date | ||
| + * with the files (text and definition) currently loaded into the editor. | ||
| + */ | ||
| + private void resetProcessors() { | ||
| + getProcessors().clear(); | ||
| + } | ||
| + | ||
| + //---- File actions ------------------------------------------------------- | ||
| + private void fileNew() { | ||
| + getFileEditorPane().newEditor(); | ||
| + } | ||
| + | ||
| + private void fileOpen() { | ||
| + getFileEditorPane().openFileDialog(); | ||
| + } | ||
| + | ||
| + private void fileClose() { | ||
| + getFileEditorPane().closeEditor( getActiveFileEditor(), true ); | ||
| + } | ||
| + | ||
| + private void fileCloseAll() { | ||
| + getFileEditorPane().closeAllEditors(); | ||
| + } | ||
| + | ||
| + private void fileSave() { | ||
| + getFileEditorPane().saveEditor( getActiveFileEditor() ); | ||
| + } | ||
| + | ||
| + private void fileSaveAll() { | ||
| + getFileEditorPane().saveAllEditors(); | ||
| + } | ||
| + | ||
| + private void fileExit() { | ||
| + final Window window = getWindow(); | ||
| + fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) ); | ||
| + } | ||
| + | ||
| + //---- Help actions ------------------------------------------------------- | ||
| + private void helpAbout() { | ||
| + Alert alert = new Alert( AlertType.INFORMATION ); | ||
| + alert.setTitle( get( "Dialog.about.title" ) ); | ||
| + alert.setHeaderText( get( "Dialog.about.header" ) ); | ||
| + alert.setContentText( get( "Dialog.about.content" ) ); | ||
| + alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) ); | ||
| + alert.initOwner( getWindow() ); | ||
| + | ||
| + alert.showAndWait(); | ||
| + } | ||
| + | ||
| + //---- Convenience accessors ---------------------------------------------- | ||
| + private float getFloat( final String key, final float defaultValue ) { | ||
| + return getPreferences().getFloat( key, defaultValue ); | ||
| + } | ||
| + | ||
| + private Preferences getPreferences() { | ||
| + return getOptions().getState(); | ||
| + } | ||
| + | ||
| + public Window getWindow() { | ||
| + return getScene().getWindow(); | ||
| + } | ||
| + | ||
| + private MarkdownEditorPane getActiveEditor() { | ||
| + final EditorPane pane = getActiveFileEditor().getEditorPane(); | ||
| + | ||
| + return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null; | ||
| + } | ||
| + | ||
| + private FileEditorTab getActiveFileEditor() { | ||
| + return getFileEditorPane().getActiveFileEditor(); | ||
| + } | ||
| + | ||
| + //---- Member accessors --------------------------------------------------- | ||
| + private void setScene( Scene scene ) { | ||
| + this.scene = scene; | ||
| + } | ||
| + | ||
| + public Scene getScene() { | ||
| + return this.scene; | ||
| + } | ||
| + | ||
| + private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) { | ||
| + this.processors = map; | ||
| + } | ||
| + | ||
| + private Map<FileEditorTab, Processor<String>> getProcessors() { | ||
| + if( this.processors == null ) { | ||
| + setProcessors( new HashMap<>() ); | ||
| + } | ||
| + | ||
| + return this.processors; | ||
| + } | ||
| + | ||
| + private FileEditorTabPane getFileEditorPane() { | ||
| + if( this.fileEditorPane == null ) { | ||
| + this.fileEditorPane = createFileEditorPane(); | ||
| + } | ||
| + | ||
| + return this.fileEditorPane; | ||
| + } | ||
| + | ||
| + private HTMLPreviewPane getPreviewPane() { | ||
| + if( this.previewPane == null ) { | ||
| + this.previewPane = createPreviewPane(); | ||
| + } | ||
| + | ||
| + return this.previewPane; | ||
| + } | ||
| + | ||
| + private void setDefinitionSource( final DefinitionSource definitionSource ) { | ||
| + this.definitionSource = definitionSource; | ||
| + } | ||
| + | ||
| + private DefinitionSource getDefinitionSource() { | ||
| + if( this.definitionSource == null ) { | ||
| + this.definitionSource = new EmptyDefinitionSource(); | ||
| + } | ||
| + | ||
| + return this.definitionSource; | ||
| + } | ||
| + | ||
| + private DefinitionPane getDefinitionPane() { | ||
| + if( this.definitionPane == null ) { | ||
| + this.definitionPane = createDefinitionPane(); | ||
| + } | ||
| + | ||
| + return this.definitionPane; | ||
| + } | ||
| + | ||
| + private Options getOptions() { | ||
| + return this.options; | ||
| + } | ||
| + | ||
| + private Snitch getSnitch() { | ||
| + return this.snitch; | ||
| + } | ||
| + | ||
| + private Notifier getNotifier() { | ||
| + return this.notifier; | ||
| + } | ||
| + | ||
| + public void setMenuBar( final MenuBar menuBar ) { | ||
| + this.menuBar = menuBar; | ||
| + } | ||
| + | ||
| + public MenuBar getMenuBar() { | ||
| + return this.menuBar; | ||
| + } | ||
| + | ||
| + private synchronized StatusBar getStatusBar() { | ||
| + if( this.statusBar == null ) { | ||
| + this.statusBar = createStatusBar(); | ||
| + } | ||
| + | ||
| + return this.statusBar; | ||
| + } | ||
| + | ||
| + //---- Member creators ---------------------------------------------------- | ||
| + /** | ||
| + * Factory to create processors that are suited to different file types. | ||
| + * | ||
| + * @param tab The tab that is subjected to processing. | ||
| + * | ||
| + * @return A processor suited to the file type specified by the tab's path. | ||
| + */ | ||
| + private Processor<String> createProcessor( final FileEditorTab tab ) { | ||
| + return createProcessorFactory().createProcessor( tab ); | ||
| + } | ||
| + | ||
| + private ProcessorFactory createProcessorFactory() { | ||
| + return new ProcessorFactory( getPreviewPane(), getResolvedMap() ); | ||
| + } | ||
| + | ||
| + private DefinitionSource createDefinitionSource( final String path ) { | ||
| + return createDefinitionFactory().createDefinitionSource( path ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Create an editor pane to hold file editor tabs. | ||
| + * | ||
| + * @return A new instance, never null. | ||
| + */ | ||
| + private FileEditorTabPane createFileEditorPane() { | ||
| + return new FileEditorTabPane(); | ||
| + } | ||
| + | ||
| + private HTMLPreviewPane createPreviewPane() { | ||
| + return new HTMLPreviewPane(); | ||
| + } | ||
| + | ||
| + private DefinitionPane createDefinitionPane() { | ||
| + return new DefinitionPane( getTreeView() ); | ||
| + } | ||
| + | ||
| + private DefinitionFactory createDefinitionFactory() { | ||
| + return new DefinitionFactory(); | ||
| + } | ||
| + | ||
| + private StatusBar createStatusBar() { | ||
| + return new StatusBar(); | ||
| + } | ||
| + | ||
| + private Node createMenuBar() { | ||
| + final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull(); | ||
| + | ||
| + // File actions | ||
| + Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() ); | ||
| + Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() ); | ||
| + Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull ); | ||
| + Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull ); | ||
| + Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(), | ||
| + createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() ); | ||
| + Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(), | ||
| + Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) ); | ||
| + Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() ); | ||
| + | ||
| + // Edit actions | ||
| + Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO, | ||
| + e -> getActiveEditor().undo(), | ||
| + createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() ); | ||
| + Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT, | ||
| + e -> getActiveEditor().redo(), | ||
| + createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() ); | ||
| + Action editFindAction = new Action( Messages.get( "Main.menu.edit.find" ), "Shortcut+F", SEARCH, | ||
| + e -> getActiveEditor().find(), | ||
| + activeFileEditorIsNull ); | ||
| + Action editReplaceAction = new Action( Messages.get( "Main.menu.edit.find.replace" ), "Shortcut+H", RETWEET, | ||
| + e -> getActiveEditor().replace(), | ||
| + activeFileEditorIsNull ); | ||
| + Action editFindNextAction = new Action( Messages.get( "Main.menu.edit.find.next" ), "F3", null, | ||
| + e -> getActiveEditor().findNext(), | ||
| + activeFileEditorIsNull ); | ||
| + Action editFindPreviousAction = new Action( Messages.get( "Main.menu.edit.find.previous" ), "Shift+F3", null, | ||
| + e -> getActiveEditor().findPrevious(), | ||
| + activeFileEditorIsNull ); | ||
| + | ||
| + // Insert actions | ||
| + Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD, | ||
| + e -> getActiveEditor().surroundSelection( "**", "**" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC, | ||
| + e -> getActiveEditor().surroundSelection( "*", "*" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertSuperscriptAction = new Action( get( "Main.menu.insert.superscript" ), "Shortcut+[", SUPERSCRIPT, | ||
| + e -> getActiveEditor().surroundSelection( "^", "^" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertSubscriptAction = new Action( get( "Main.menu.insert.subscript" ), "Shortcut+]", SUBSCRIPT, | ||
| + e -> getActiveEditor().surroundSelection( "~", "~" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH, | ||
| + e -> getActiveEditor().surroundSelection( "~~", "~~" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac | ||
| + e -> getActiveEditor().surroundSelection( "\n\n> ", "" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE, | ||
| + e -> getActiveEditor().surroundSelection( "`", "`" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT, | ||
| + e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ), | ||
| + activeFileEditorIsNull ); | ||
| + | ||
| + Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK, | ||
| + e -> getActiveEditor().insertLink(), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT, | ||
| + e -> getActiveEditor().insertImage(), | ||
| + activeFileEditorIsNull ); | ||
| + | ||
| + final Action[] headers = new Action[ 6 ]; | ||
| + | ||
| + // Insert header actions (H1 ... H6) | ||
| + for( int i = 1; i <= 6; i++ ) { | ||
| + final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | ||
| + final String markup = String.format( "%n%n%s ", hashes ); | ||
| + final String text = get( "Main.menu.insert.header_" + i ); | ||
| + final String accelerator = "Shortcut+" + i; | ||
| + final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" ); | ||
| + | ||
| + headers[ i - 1 ] = new Action( text, accelerator, HEADER, | ||
| + e -> getActiveEditor().surroundSelection( markup, "", prompt ), | ||
| + activeFileEditorIsNull ); | ||
| + } | ||
| + | ||
| + Action insertUnorderedListAction = new Action( get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL, | ||
| + e -> getActiveEditor().surroundSelection( "\n\n* ", "" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertOrderedListAction = new Action( get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL, | ||
| + e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertHorizontalRuleAction = new Action( get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null, | ||
| + e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ), | ||
| + activeFileEditorIsNull ); | ||
| + | ||
| + // Help actions | ||
| + Action helpAboutAction = new Action( get( "Main.menu.help.about" ), null, null, e -> helpAbout() ); | ||
| + | ||
| + //---- MenuBar ---- | ||
| + Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ), | ||
| + fileNewAction, | ||
| + fileOpenAction, | ||
| + null, | ||
| + fileCloseAction, | ||
| + fileCloseAllAction, | ||
| + null, | ||
| + fileSaveAction, | ||
| + fileSaveAllAction, | ||
| + null, | ||
| + fileExitAction ); | ||
| + | ||
| + Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ), | ||
| + editUndoAction, | ||
| + editRedoAction, | ||
| + editFindAction, | ||
| + editReplaceAction, | ||
| + editFindNextAction, | ||
| + editFindPreviousAction ); | ||
| + | ||
| + Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ), | ||
| + insertBoldAction, | ||
| + insertItalicAction, | ||
| + insertSuperscriptAction, | ||
| + insertSubscriptAction, | ||
| + insertStrikethroughAction, | ||
| + insertBlockquoteAction, | ||
| + insertCodeAction, | ||
| + insertFencedCodeBlockAction, | ||
| + null, | ||
| + insertLinkAction, | ||
| + insertImageAction, | ||
| + null, | ||
| + headers[ 0 ], | ||
| + headers[ 1 ], | ||
| + headers[ 2 ], | ||
| + headers[ 3 ], | ||
| + headers[ 4 ], | ||
| + headers[ 5 ], | ||
| + null, | ||
| + insertUnorderedListAction, | ||
| + insertOrderedListAction, | ||
| + insertHorizontalRuleAction ); | ||
| + | ||
| + Menu helpMenu = ActionUtils.createMenu( 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, | ||
| + insertSuperscriptAction, | ||
| + insertSubscriptAction, | ||
| + insertBlockquoteAction, | ||
| + insertCodeAction, | ||
| + insertFencedCodeBlockAction, | ||
| + null, | ||
| + insertLinkAction, | ||
| + insertImageAction, | ||
| + null, | ||
| + headers[ 0 ], | ||
| + null, | ||
| + insertUnorderedListAction, | ||
| + insertOrderedListAction ); | ||
| + | ||
| + return new VBox( menuBar, toolBar ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Creates a boolean property that is bound to another boolean value of the | ||
| + * active editor. | ||
| + */ | ||
| + private BooleanProperty createActiveBooleanProperty( | ||
| + final Function<FileEditorTab, ObservableBooleanValue> func ) { | ||
| + | ||
| + final BooleanProperty b = new SimpleBooleanProperty(); | ||
| + final FileEditorTab tab = getActiveFileEditor(); | ||
| + | ||
| + if( tab != null ) { | ||
| + b.bind( func.apply( tab ) ); | ||
| + } | ||
| + | ||
| + getFileEditorPane().activeFileEditorProperty().addListener( | ||
| + (observable, oldFileEditor, newFileEditor) -> { | ||
| + b.unbind(); | ||
| + | ||
| + if( newFileEditor != null ) { | ||
| + b.bind( func.apply( newFileEditor ) ); | ||
| + } else { | ||
| + b.set( false ); | ||
| + } | ||
| + } | ||
| + ); | ||
| + | ||
| + return b; | ||
| + } | ||
| + | ||
| + private void initLayout() { | ||
| + final SplitPane splitPane = new SplitPane( | ||
| + getDefinitionPane().getNode(), | ||
| + getFileEditorPane().getNode(), | ||
| + getPreviewPane().getNode() ); | ||
| + | ||
| + splitPane.setDividerPositions( | ||
| + getFloat( K_PANE_SPLIT_DEFINITION, .10f ), | ||
| + getFloat( K_PANE_SPLIT_EDITOR, .45f ), | ||
| + getFloat( K_PANE_SPLIT_PREVIEW, .45f ) ); | ||
| + | ||
| + // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html | ||
| + final BorderPane borderPane = new BorderPane(); | ||
| + borderPane.setPrefSize( 1024, 800 ); | ||
| + borderPane.setTop( createMenuBar() ); | ||
| + borderPane.setBottom( getStatusBar() ); | ||
| borderPane.setCenter( splitPane ); | ||
| package com.scrivenvar; | ||
| +import static com.scrivenvar.Constants.APP_BUNDLE_NAME; | ||
| import java.text.MessageFormat; | ||
| import java.util.ResourceBundle; | ||
| import java.util.Stack; | ||
| -import static com.scrivenvar.Constants.APP_BUNDLE_NAME; | ||
| /** | ||
| try { | ||
| result = resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) ); | ||
| - } catch( Exception e ) { | ||
| - | ||
| - // Instead of crashing, launch the application and show the resource | ||
| - // name. | ||
| + } catch( final Exception ex ) { | ||
| result = key; | ||
| } | ||
| try { | ||
| result = file.toURI().toURL().getProtocol(); | ||
| - } catch( Exception e ) { | ||
| + } catch( final Exception e ) { | ||
| result = DEFINITION_PROTOCOL_UNKNOWN; | ||
| } |
| package com.scrivenvar.definition.yaml; | ||
| -import com.scrivenvar.definition.FileDefinitionSource; | ||
| import static com.scrivenvar.Messages.get; | ||
| +import com.scrivenvar.definition.FileDefinitionSource; | ||
| import java.io.InputStream; | ||
| import java.nio.file.Files; | ||
| try( final InputStream in = Files.newInputStream( getPath() ) ) { | ||
| return new YamlParser( in ); | ||
| - } catch( final Exception e ) { | ||
| - throw new RuntimeException( e ); | ||
| + } catch( final Exception ex ) { | ||
| + throw new RuntimeException( ex ); | ||
| } | ||
| } | ||
| import org.fxmisc.wellbehaved.event.EventPattern; | ||
| import org.fxmisc.wellbehaved.event.InputMap; | ||
| -import static org.fxmisc.wellbehaved.event.InputMap.consume; | ||
| import org.fxmisc.wellbehaved.event.Nodes; | ||
| +import static org.fxmisc.wellbehaved.event.InputMap.consume; | ||
| /** | ||
| public void redo() { | ||
| getUndoManager().redo(); | ||
| + } | ||
| + | ||
| + public void find() { | ||
| + System.out.println( "search" ); | ||
| + } | ||
| + | ||
| + public void replace() { | ||
| + System.out.println( "replace" ); | ||
| + } | ||
| + | ||
| + public void findNext() { | ||
| + System.out.println( "find next" ); | ||
| + } | ||
| + | ||
| + public void findPrevious() { | ||
| + System.out.println( "find previous" ); | ||
| } | ||
| import static org.fxmisc.wellbehaved.event.InputMap.consume; | ||
| import static org.fxmisc.wellbehaved.event.InputMap.sequence; | ||
| +import static com.scrivenvar.util.Lists.getFirst; | ||
| +import static com.scrivenvar.util.Lists.getLast; | ||
| +import static java.lang.Character.isSpaceChar; | ||
| +import static java.lang.Character.isWhitespace; | ||
| +import static java.lang.Math.min; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped; | ||
| +import static org.fxmisc.wellbehaved.event.InputMap.consume; | ||
| /** |
| import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| /** |
| sync(); | ||
| } catch( final BackingStoreException ex ) { | ||
| - problem( ex ); | ||
| + error( ex ); | ||
| } | ||
| } | ||
| flush(); | ||
| } catch( final BackingStoreException ex ) { | ||
| - problem( ex ); | ||
| + error( ex ); | ||
| } | ||
| } | ||
| flush(); | ||
| } catch( final BackingStoreException ex ) { | ||
| - problem( ex ); | ||
| + error( ex ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| - } catch( final IOException e ) { | ||
| - throw new BackingStoreException( e ); | ||
| + } catch( final IOException ex ) { | ||
| + error( new BackingStoreException( ex ) ); | ||
| } | ||
| } | ||
| p.store( new FileOutputStream( file ), "FilePreferences" ); | ||
| - } catch( final IOException e ) { | ||
| - throw new BackingStoreException( e ); | ||
| + } catch( final IOException ex ) { | ||
| + error( new BackingStoreException( ex ) ); | ||
| } | ||
| } | ||
| } | ||
| - private void problem( final BackingStoreException ex ) { | ||
| + private void error( final BackingStoreException ex ) { | ||
| throw new RuntimeException( ex ); | ||
| } | ||
| protected String inject( final String text, final int i ) { | ||
| if( i > 0 && i <= text.length() ) { | ||
| + // Preserve the newline character when inserting the caret position mark. | ||
| final String replacement = text.charAt( i - 1 ) == NEWLINE | ||
| ? NEWLINE_CARET_POSITION_MD |
| } else { | ||
| + // TODO: Implement this. | ||
| // There was a starting prefix but no ending suffix. Ignore the | ||
| // problem, copy to the end, and exit the loop. | ||
| -// sb.append() | ||
| - | ||
| + //sb.append() | ||
| } | ||
| return getScriptEngine().eval( r ); | ||
| } catch( final ScriptException ex ) { | ||
| - problem( ex ); | ||
| + throw new IllegalArgumentException( ex ); | ||
| } | ||
| - | ||
| - return ""; | ||
| } | ||
| private synchronized ScriptEngine getScriptEngine() { | ||
| if( this.engine == null ) { | ||
| this.engine = (new ScriptEngineManager()).getEngineByName( "Renjin" ); | ||
| } | ||
| return this.engine; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Notify the user (passively) of the problem. | ||
| - * | ||
| - * @param ex A problem parsing the text. | ||
| - */ | ||
| - private void problem( final Exception ex ) { | ||
| - // TODO: Use the notify service to warn the user that there's an issue. | ||
| - System.out.println( ex ); | ||
| } | ||
| } | ||
| // Search for inline R code from the start of the caret's paragraph. | ||
| + // This should be much faster than scanning text from the beginning. | ||
| int index = text.lastIndexOf( NEWLINE, offset ); | ||
| // beyond the caret position. | ||
| while( index != INDEX_NOT_FOUND && index < offset ) { | ||
| - // Set rPrefix to the index that might precede the caret. | ||
| + // Set rPrefix to the index that might precede the caret. The + 1 is | ||
| + // to skip passed the leading backtick in the prefix (`r#). | ||
| rPrefix = index + 1; | ||
| final int rSuffix = max( text.indexOf( SUFFIX, rPrefix ), rPrefix ); | ||
| + // If the caret falls between the rPrefix and rSuffix, then change the | ||
| + // insertion point. | ||
| final boolean between = isBetween( offset, rPrefix, rSuffix ); | ||
| try { | ||
| return text.isEmpty() ? text : transform( text ); | ||
| - } catch( Exception e ) { | ||
| - throw new RuntimeException( e ); | ||
| + } catch( final Exception ex ) { | ||
| + throw new RuntimeException( ex ); | ||
| } | ||
| } |
| +/* | ||
| + * Copyright 2016 White Magic Software, Ltd. | ||
| + * | ||
| + * All rights reserved. | ||
| + * | ||
| + * Redistribution and use in source and binary forms, with or without | ||
| + * modification, are permitted provided that the following conditions are met: | ||
| + * | ||
| + * o Redistributions of source code must retain the above copyright | ||
| + * notice, this list of conditions and the following disclaimer. | ||
| + * | ||
| + * o Redistributions in binary form must reproduce the above copyright | ||
| + * notice, this list of conditions and the following disclaimer in the | ||
| + * documentation and/or other materials provided with the distribution. | ||
| + * | ||
| + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
| + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
| + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
| + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
| + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
| + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
| + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
| + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
| + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
| + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| + */ | ||
| +package com.scrivenvar.service.events; | ||
| + | ||
| +import java.util.Observer; | ||
| +import javafx.scene.control.Alert; | ||
| +import javafx.scene.control.ButtonType; | ||
| +import javafx.stage.Window; | ||
| + | ||
| +/** | ||
| + * Provides the application with a uniform way to notify the user of events. | ||
| + * | ||
| + * @author White Magic Software, Ltd. | ||
| + */ | ||
| +public interface Notifier { | ||
| + | ||
| + public static final ButtonType YES = ButtonType.YES; | ||
| + public static final ButtonType NO = ButtonType.NO; | ||
| + public static final ButtonType CANCEL = ButtonType.CANCEL; | ||
| + | ||
| + /** | ||
| + * Notifies the user of a problem. | ||
| + * | ||
| + * @param message The problem description. | ||
| + */ | ||
| + public void notify( final String message ); | ||
| + | ||
| + /** | ||
| + * Notifies the user about the exception. | ||
| + * | ||
| + * @param exception The exception containing a message to show to the user. | ||
| + */ | ||
| + default public void notify( final Exception exception ) { | ||
| + notify( exception.getMessage() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Causes any displayed notifications to disappear. | ||
| + */ | ||
| + public void clear(); | ||
| + | ||
| + /** | ||
| + * Constructs a default alert message text for a modal alert dialog. | ||
| + * | ||
| + * @param title The dialog box message title. | ||
| + * @param message The dialog box message content (needs formatting). | ||
| + * @param args The arguments to the message content that must be formatted. | ||
| + * | ||
| + * @return The message suitable for building a modal alert dialog. | ||
| + */ | ||
| + public Notification createNotification( | ||
| + String title, | ||
| + String message, | ||
| + Object... args ); | ||
| + | ||
| + /** | ||
| + * Creates an alert of alert type error with a message showing the cause of | ||
| + * the error. | ||
| + * | ||
| + * @param parent Dialog box owner (for modal purposes). | ||
| + * @param message The error message, title, and possibly more details. | ||
| + * | ||
| + * @return A modal alert dialog box ready to display using showAndWait. | ||
| + */ | ||
| + public Alert createError( Window parent, Notification message ); | ||
| + | ||
| + /** | ||
| + * Creates an alert of alert type confirmation with Yes/No/Cancel buttons. | ||
| + * | ||
| + * @param parent Dialog box owner (for modal purposes). | ||
| + * @param message The message, title, and possibly more details. | ||
| + * | ||
| + * @return A modal alert dialog box ready to display using showAndWait. | ||
| + */ | ||
| + public Alert createConfirmation( Window parent, Notification message ); | ||
| + | ||
| + /** | ||
| + * Adds an observer to the list of objects that receive notifications about | ||
| + * error messages to be presented to the user. | ||
| + * | ||
| + * @param observer The observer instance to notify. | ||
| + */ | ||
| + public void addObserver( Observer observer ); | ||
| + | ||
| + /** | ||
| + * Removes an observer from the list of objects that receive notifications | ||
| + * about error messages to be presented to the user. | ||
| + * | ||
| + * @param observer The observer instance to no longer notify. | ||
| + */ | ||
| + public void deleteObserver( Observer observer ); | ||
| +} | ||
| -/* | ||
| - * Copyright 2016 White Magic Software, Ltd. | ||
| - * | ||
| - * All rights reserved. | ||
| - * | ||
| - * Redistribution and use in source and binary forms, with or without | ||
| - * modification, are permitted provided that the following conditions are met: | ||
| - * | ||
| - * o Redistributions of source code must retain the above copyright | ||
| - * notice, this list of conditions and the following disclaimer. | ||
| - * | ||
| - * o Redistributions in binary form must reproduce the above copyright | ||
| - * notice, this list of conditions and the following disclaimer in the | ||
| - * documentation and/or other materials provided with the distribution. | ||
| - * | ||
| - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
| - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
| - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
| - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
| - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
| - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
| - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
| - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
| - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
| - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| - */ | ||
| -package com.scrivenvar.service.events; | ||
| - | ||
| -import javafx.scene.control.Alert; | ||
| -import javafx.scene.control.ButtonType; | ||
| -import javafx.stage.Window; | ||
| - | ||
| -/** | ||
| - * Provides the application with a uniform way to notify the user of events. | ||
| - * | ||
| - * @author White Magic Software, Ltd. | ||
| - */ | ||
| -public interface NotifyService { | ||
| - public static final ButtonType YES = ButtonType.YES; | ||
| - public static final ButtonType NO = ButtonType.NO; | ||
| - public static final ButtonType CANCEL = ButtonType.CANCEL; | ||
| - | ||
| - /** | ||
| - * Called to set the window used as the parent for the alert dialogs. | ||
| - * | ||
| - * @param window | ||
| - */ | ||
| - public void setWindow( Window window ); | ||
| - | ||
| - /** | ||
| - * Constructs a default alert message text for a modal alert dialog. | ||
| - * | ||
| - * @param title The dialog box message title. | ||
| - * @param message The dialog box message content (needs formatting). | ||
| - * @param args The arguments to the message content that must be formatted. | ||
| - * | ||
| - * @return The message suitable for building a modal alert dialog. | ||
| - */ | ||
| - public Notification createNotification( | ||
| - String title, | ||
| - String message, | ||
| - Object... args ); | ||
| - | ||
| - /** | ||
| - * Creates an alert of alert type error with a message showing the cause of | ||
| - * the error. | ||
| - * | ||
| - * @param message The error message, title, and possibly more details. | ||
| - * | ||
| - * @return A modal alert dialog box ready to display using showAndWait. | ||
| - */ | ||
| - public Alert createError( Notification message ); | ||
| - | ||
| - /** | ||
| - * Creates an alert of alert type confirmation with Yes/No/Cancel buttons. | ||
| - * | ||
| - * @param message The message, title, and possibly more details. | ||
| - * | ||
| - * @return A modal alert dialog box ready to display using showAndWait. | ||
| - */ | ||
| - public Alert createConfirmation( Notification message ); | ||
| -} | ||
| +/* | ||
| + * Copyright 2016 White Magic Software, Ltd. | ||
| + * | ||
| + * All rights reserved. | ||
| + * | ||
| + * Redistribution and use in source and binary forms, with or without | ||
| + * modification, are permitted provided that the following conditions are met: | ||
| + * | ||
| + * o Redistributions of source code must retain the above copyright | ||
| + * notice, this list of conditions and the following disclaimer. | ||
| + * | ||
| + * o Redistributions in binary form must reproduce the above copyright | ||
| + * notice, this list of conditions and the following disclaimer in the | ||
| + * documentation and/or other materials provided with the distribution. | ||
| + * | ||
| + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
| + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
| + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
| + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
| + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
| + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
| + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
| + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
| + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
| + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| + */ | ||
| +package com.scrivenvar.service.events.impl; | ||
| + | ||
| +import com.scrivenvar.service.events.Notification; | ||
| +import com.scrivenvar.service.events.Notifier; | ||
| +import java.util.Observable; | ||
| +import javafx.scene.control.Alert; | ||
| +import javafx.scene.control.Alert.AlertType; | ||
| +import static javafx.scene.control.Alert.AlertType.CONFIRMATION; | ||
| +import static javafx.scene.control.Alert.AlertType.ERROR; | ||
| +import javafx.stage.Window; | ||
| + | ||
| +/** | ||
| + * Provides the ability to notify the user of problems. | ||
| + * | ||
| + * @author White Magic Software, Ltd. | ||
| + */ | ||
| +public final class DefaultNotifier extends Observable | ||
| + implements Notifier { | ||
| + | ||
| + public DefaultNotifier() { | ||
| + } | ||
| + | ||
| + /** | ||
| + * Notifies all observer instances of the given message. | ||
| + * | ||
| + * @param message The text to display to the user. | ||
| + */ | ||
| + @Override | ||
| + public void notify( final String message ) { | ||
| + setChanged(); | ||
| + notifyObservers( message ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Contains all the information that the user needs to know about a problem. | ||
| + * | ||
| + * @param title The context for the message. | ||
| + * @param message The message content (formatted with the given args). | ||
| + * @param args Parameters for the message content. | ||
| + * | ||
| + * @return | ||
| + */ | ||
| + @Override | ||
| + public Notification createNotification( | ||
| + final String title, | ||
| + final String message, | ||
| + final Object... args ) { | ||
| + return new DefaultNotification( title, message, args ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void clear() { | ||
| + setChanged(); | ||
| + notifyObservers( "OK" ); | ||
| + } | ||
| + | ||
| + private Alert createAlertDialog( | ||
| + final Window parent, | ||
| + final AlertType alertType, | ||
| + final Notification message ) { | ||
| + | ||
| + final Alert alert = new Alert( alertType ); | ||
| + | ||
| + alert.setDialogPane( new ButtonOrderPane() ); | ||
| + alert.setTitle( message.getTitle() ); | ||
| + alert.setHeaderText( null ); | ||
| + alert.setContentText( message.getContent() ); | ||
| + alert.initOwner( parent ); | ||
| + | ||
| + return alert; | ||
| + } | ||
| + | ||
| + @Override | ||
| + public Alert createConfirmation( final Window parent, final Notification message ) { | ||
| + final Alert alert = createAlertDialog( parent, CONFIRMATION, message ); | ||
| + | ||
| + alert.getButtonTypes().setAll( YES, NO, CANCEL ); | ||
| + | ||
| + return alert; | ||
| + } | ||
| + | ||
| + @Override | ||
| + public Alert createError( final Window parent, final Notification message ) { | ||
| + return createAlertDialog( parent, ERROR, message ); | ||
| + } | ||
| +} | ||
| -/* | ||
| - * Copyright 2016 White Magic Software, Ltd. | ||
| - * | ||
| - * All rights reserved. | ||
| - * | ||
| - * Redistribution and use in source and binary forms, with or without | ||
| - * modification, are permitted provided that the following conditions are met: | ||
| - * | ||
| - * o Redistributions of source code must retain the above copyright | ||
| - * notice, this list of conditions and the following disclaimer. | ||
| - * | ||
| - * o Redistributions in binary form must reproduce the above copyright | ||
| - * notice, this list of conditions and the following disclaimer in the | ||
| - * documentation and/or other materials provided with the distribution. | ||
| - * | ||
| - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
| - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
| - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
| - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
| - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
| - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
| - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
| - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
| - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
| - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| - */ | ||
| -package com.scrivenvar.service.events.impl; | ||
| - | ||
| -import com.scrivenvar.service.events.NotifyService; | ||
| -import javafx.scene.control.Alert; | ||
| -import javafx.scene.control.Alert.AlertType; | ||
| -import static javafx.scene.control.Alert.AlertType.CONFIRMATION; | ||
| -import static javafx.scene.control.Alert.AlertType.ERROR; | ||
| -import javafx.stage.Window; | ||
| -import com.scrivenvar.service.events.Notification; | ||
| - | ||
| -/** | ||
| - * Provides the ability to notify the user of problems. | ||
| - * | ||
| - * @author White Magic Software, Ltd. | ||
| - */ | ||
| -public final class DefaultNotifyService implements NotifyService { | ||
| - | ||
| - private Window window; | ||
| - | ||
| - public DefaultNotifyService() { | ||
| - } | ||
| - | ||
| - public DefaultNotifyService( final Window window ) { | ||
| - this.window = window; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Contains all the information that the user needs to know about a problem. | ||
| - * | ||
| - * @param title The context for the message. | ||
| - * @param message The message content (formatted with the given args). | ||
| - * @param args Parameters for the message content. | ||
| - * @return | ||
| - */ | ||
| - @Override | ||
| - public Notification createNotification( | ||
| - final String title, | ||
| - final String message, | ||
| - final Object... args ) { | ||
| - return new DefaultNotification( title, message, args ); | ||
| - } | ||
| - | ||
| - private Alert createAlertDialog( | ||
| - final AlertType alertType, | ||
| - final Notification message ) { | ||
| - | ||
| - final Alert alert = new Alert( alertType ); | ||
| - | ||
| - alert.setDialogPane( new ButtonOrderPane() ); | ||
| - alert.setTitle( message.getTitle() ); | ||
| - alert.setHeaderText( null ); | ||
| - alert.setContentText( message.getContent() ); | ||
| - alert.initOwner( getWindow() ); | ||
| - | ||
| - return alert; | ||
| - } | ||
| - | ||
| - @Override | ||
| - public Alert createConfirmation( final Notification message ) { | ||
| - final Alert alert = createAlertDialog( CONFIRMATION, message ); | ||
| - | ||
| - alert.getButtonTypes().setAll( YES, NO, CANCEL ); | ||
| - | ||
| - return alert; | ||
| - } | ||
| - | ||
| - @Override | ||
| - public Alert createError( final Notification message ) { | ||
| - return createAlertDialog( ERROR, message ); | ||
| - } | ||
| - | ||
| - private Window getWindow() { | ||
| - return this.window; | ||
| - } | ||
| - | ||
| - @Override | ||
| - public void setWindow( Window window ) { | ||
| - this.window = window; | ||
| - } | ||
| -} | ||
| configuration.read( r ); | ||
| - } catch( IOException e ) { | ||
| - throw new ConfigurationException( e ); | ||
| + } catch( final IOException ex ) { | ||
| + throw new RuntimeException( new ConfigurationException( ex ) ); | ||
| } | ||
| } |
| ignore( path ); | ||
| } | ||
| - } catch( IOException | InterruptedException ex ) { | ||
| + } catch( final IOException | InterruptedException ex ) { | ||
| // Stop eavesdropping. | ||
| setListening( false ); |
| - | ||
| +com.scrivenvar.service.events.impl.DefaultNotifier |
| -com.scrivenvar.service.events.impl.DefaultNotifyService | ||
| + |
| Main.menu.edit=_Edit | ||
| -Main.menu.edit.undo=Undo | ||
| -Main.menu.edit.redo=Redo | ||
| +Main.menu.edit.undo=_Undo | ||
| +Main.menu.edit.redo=_Redo | ||
| +Main.menu.edit.find=_Find | ||
| +Main.menu.edit.find.replace=Re_place | ||
| +Main.menu.edit.find.next=Find _Next | ||
| +Main.menu.edit.find.previous=Find _Previous | ||
| Main.menu.insert=_Insert |
| Delta | 1990 lines added, 1859 lines removed, 131-line increase |
|---|