Dave Jarvis' Repositories

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

Added status bar. Most application exceptions now show as status bar messages.

Authordjarvis <email>
Date2016-12-27 22:16:07 GMT-0800
Commit6841785c3ea1401c0a50ecb02ebecc9c6e20c3e9
Parentf38bac3
build.gradle
-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'
src/main/java/com/scrivenvar/AbstractFileFactory.java
* @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;
}
-
}
src/main/java/com/scrivenvar/FileEditorTab.java
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;
}
src/main/java/com/scrivenvar/FileEditorTabPane.java
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 ) );
src/main/java/com/scrivenvar/Main.java
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;
}
src/main/java/com/scrivenvar/MainWindow.java
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 );
src/main/java/com/scrivenvar/Messages.java
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;
}
src/main/java/com/scrivenvar/definition/DefinitionFactory.java
try {
result = file.toURI().toURL().getProtocol();
- } catch( Exception e ) {
+ } catch( final Exception e ) {
result = DEFINITION_PROTOCOL_UNKNOWN;
}
src/main/java/com/scrivenvar/definition/yaml/YamlFileDefinitionSource.java
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 );
}
}
src/main/java/com/scrivenvar/editors/EditorPane.java
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" );
}
src/main/java/com/scrivenvar/editors/VariableNameInjector.java
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;
/**
src/main/java/com/scrivenvar/editors/markdown/MarkdownEditorPane.java
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;
/**
src/main/java/com/scrivenvar/preferences/FilePreferences.java
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 );
}
src/main/java/com/scrivenvar/processors/CaretInsertionProcessor.java
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
src/main/java/com/scrivenvar/processors/InlineRProcessor.java
} 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 );
}
}
src/main/java/com/scrivenvar/processors/RMarkdownCaretInsertionProcessor.java
// 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 );
src/main/java/com/scrivenvar/processors/XMLProcessor.java
try {
return text.isEmpty() ? text : transform( text );
- } catch( Exception e ) {
- throw new RuntimeException( e );
+ } catch( final Exception ex ) {
+ throw new RuntimeException( ex );
}
}
src/main/java/com/scrivenvar/service/events/Notifier.java
+/*
+ * 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 );
+}
src/main/java/com/scrivenvar/service/events/NotifyService.java
-/*
- * 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 );
-}
src/main/java/com/scrivenvar/service/events/impl/DefaultNotifier.java
+/*
+ * 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 );
+ }
+}
src/main/java/com/scrivenvar/service/events/impl/DefaultNotifyService.java
-/*
- * 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;
- }
-}
src/main/java/com/scrivenvar/service/impl/DefaultSettings.java
configuration.read( r );
- } catch( IOException e ) {
- throw new ConfigurationException( e );
+ } catch( final IOException ex ) {
+ throw new RuntimeException( new ConfigurationException( ex ) );
}
}
src/main/java/com/scrivenvar/service/impl/DefaultSnitch.java
ignore( path );
}
- } catch( IOException | InterruptedException ex ) {
+ } catch( final IOException | InterruptedException ex ) {
// Stop eavesdropping.
setListening( false );
src/main/resources/META-INF/services/com.scrivenvar.service.events.Notifier
-
+com.scrivenvar.service.events.impl.DefaultNotifier
src/main/resources/META-INF/services/com.scrivenvar.service.events.NotifyService
-com.scrivenvar.service.events.impl.DefaultNotifyService
+
src/main/resources/com/scrivenvar/messages.properties
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
Delta1990 lines added, 1859 lines removed, 131-line increase