Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M CREDITS.md
1010
  * Jens Deters: [FontAwesomeFX](https://bitbucket.org/Jerady/fontawesomefx)
1111
  * Shy Shalom, Kohei Taketa: [juniversalchardet](https://github.com/takscape/juniversalchardet)
12
12
 * David Croft, [File Preferences](https://github.com/eis/simple-suomi24-java-client/tree/master/src/main/java/net/infotrek/util/prefs)
M README.md
2222
* Spell check
2323
* Search and replace, with or without variables
24
* R integration using [Rserve](https://rforge.net/Rserve/)
24
* R integration
2525
* Re-organize variable names
2626
M src/main/java/com/scrivenvar/AbstractFileFactory.java
5353
   * @return The file type that corresponds to the given path.
5454
   */
55
  protected FileType lookup( final Path path, final String prefix ) {
55
    protected FileType lookup( final Path path, final String prefix ) {
5656
    final Settings properties = getSettings();
5757
    final Iterator<String> keys = properties.getKeys( prefix );
M src/main/java/com/scrivenvar/AbstractPane.java
2828
package com.scrivenvar;
2929
30
import com.scrivenvar.Services;
3130
import com.scrivenvar.service.Options;
3231
import java.util.prefs.Preferences;
M src/main/java/com/scrivenvar/Constants.java
5353
  // Bootstrapping...
5454
  public static final String SETTINGS_NAME = "/com/scrivenvar/settings.properties";
55
55
  
56
  public static final String APP_TITLE = get( "application.title" );
5657
  public static final String APP_BUNDLE_NAME = get( "application.messages" );
5758
  public static final int APP_WATCHDOG_TIMEOUT = get( "application.watchdog.timeout", 100 );
...
7475
  public static final String PREFS_STATE = get( "preferences.root.state" );
7576
  public static final String PREFS_OPTIONS = get( "preferences.root.options" );
76
  public static final String PREFS_DEFINITION_SOURCE = get( "preferences.root.definition.source" );
77
  public static final String PREFS_DEFINITION_SOURCE = get( "preferences.root.definitionSource" );
7778
7879
  // Refer to filename extension settings in the configuration file. Do not
M src/main/java/com/scrivenvar/FileEditorTab.java
2828
import com.scrivenvar.editors.EditorPane;
2929
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
30
import com.scrivenvar.service.events.AlertMessage;
31
import com.scrivenvar.service.events.AlertService;
32
import java.nio.charset.Charset;
33
import java.nio.file.Files;
34
import java.nio.file.Path;
35
import static java.util.Locale.ENGLISH;
36
import java.util.function.Consumer;
37
import javafx.application.Platform;
38
import javafx.beans.binding.Bindings;
39
import javafx.beans.property.BooleanProperty;
40
import javafx.beans.property.ReadOnlyBooleanProperty;
41
import javafx.beans.property.ReadOnlyBooleanWrapper;
42
import javafx.beans.property.SimpleBooleanProperty;
43
import javafx.beans.value.ChangeListener;
44
import javafx.beans.value.ObservableValue;
45
import javafx.event.Event;
46
import javafx.scene.Node;
47
import javafx.scene.control.Tab;
48
import javafx.scene.control.Tooltip;
49
import javafx.scene.input.InputEvent;
50
import javafx.scene.text.Text;
51
import org.fxmisc.richtext.StyleClassedTextArea;
52
import org.fxmisc.undo.UndoManager;
53
import org.fxmisc.wellbehaved.event.EventPattern;
54
import org.fxmisc.wellbehaved.event.InputMap;
55
import org.mozilla.universalchardet.UniversalDetector;
56
57
/**
58
 * Editor for a single file.
59
 *
60
 * @author Karl Tauber and White Magic Software, Ltd.
61
 */
62
public final class FileEditorTab extends Tab {
63
64
  private final AlertService alertService = Services.load( AlertService.class );
65
  private EditorPane editorPane;
66
67
  /**
68
   * Character encoding used by the file (or default encoding if none found).
69
   */
70
  private Charset encoding;
71
72
  private final ReadOnlyBooleanWrapper modified = new ReadOnlyBooleanWrapper();
73
  private final BooleanProperty canUndo = new SimpleBooleanProperty();
74
  private final BooleanProperty canRedo = new SimpleBooleanProperty();
75
  private Path path;
76
77
  FileEditorTab( final Path path ) {
78
    setPath( path );
79
80
    this.modified.addListener( (observable, oldPath, newPath) -> updateTab() );
81
    updateTab();
82
83
    setOnSelectionChanged( e -> {
84
      if( isSelected() ) {
85
        Platform.runLater( () -> activated() );
86
      }
87
    } );
88
  }
89
90
  private void updateTab() {
91
    setText( getTabTitle() );
92
    setGraphic( getModifiedMark() );
93
    setTooltip( getTabTooltip() );
94
  }
95
96
  /**
97
   * Returns the base filename (without the directory names).
98
   *
99
   * @return The untitled text if the path hasn't been set.
100
   */
101
  private String getTabTitle() {
102
    final Path filePath = getPath();
103
104
    return (filePath == null)
105
      ? Messages.get( "FileEditor.untitled" )
106
      : filePath.getFileName().toString();
107
  }
108
109
  /**
110
   * Returns the full filename represented by the path.
111
   *
112
   * @return The untitled text if the path hasn't been set.
113
   */
114
  private Tooltip getTabTooltip() {
115
    final Path filePath = getPath();
116
117
    return (filePath == null)
118
      ? null
119
      : new Tooltip( filePath.toString() );
120
  }
121
122
  /**
123
   * Returns a marker to indicate whether the file has been modified.
124
   *
125
   * @return "*" when the file has changed; otherwise null.
126
   */
127
  private Text getModifiedMark() {
128
    return isModified() ? new Text( "*" ) : null;
129
  }
130
131
  /**
132
   * Called when the user switches tab.
133
   */
134
  private void activated() {
135
    // Tab is closed or no longer active.
136
    if( getTabPane() == null || !isSelected() ) {
137
      return;
138
    }
139
140
    // Switch to the tab without loading if the contents are already in memory.
141
    if( getContent() != null ) {
142
      getEditorPane().requestFocus();
143
      return;
144
    }
145
146
    // Load the text and update the preview before the undo manager.
147
    load();
148
149
    // Track undo requests -- can only be called *after* load.
150
    initUndoManager();
151
    initLayout();
152
    initFocus();
153
  }
154
155
  private void initLayout() {
156
    setContent( getScrollPane() );
157
  }
158
159
  private Node getScrollPane() {
160
    return getEditorPane().getScrollPane();
161
  }
162
163
  private void initFocus() {
164
    getEditorPane().requestFocus();
165
  }
166
167
  private void initUndoManager() {
168
    final UndoManager undoManager = getUndoManager();
169
170
    // Clear undo history after first load.
171
    undoManager.forgetHistory();
172
173
    // Bind the editor undo manager to the properties.
174
    modified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
175
    canUndo.bind( undoManager.undoAvailableProperty() );
176
    canRedo.bind( undoManager.redoAvailableProperty() );
177
  }
178
179
  /**
180
   * Returns the index into the text where the caret blinks happily away.
181
   *
182
   * @return A number from 0 to the editor's document text length.
183
   */
184
  public int getCaretPosition() {
185
    return getEditor().getCaretPosition();
186
  }
187
188
  /**
189
   * Allows observers to synchronize caret position changes.
190
   *
191
   * @return An observable caret property value.
192
   */
193
  public final ObservableValue<Integer> caretPositionProperty() {
194
    return getEditor().caretPositionProperty();
195
  }
196
197
  /**
198
   * Returns the text area associated with this tab.
199
   *
200
   * @return A text editor.
201
   */
202
  private StyleClassedTextArea getEditor() {
203
    return getEditorPane().getEditor();
204
  }
205
206
  /**
207
   * Returns true if the given path exactly matches this tab's path.
208
   *
209
   * @param check The path to compare against.
210
   *
211
   * @return true The paths are the same.
212
   */
213
  public boolean isPath( final Path check ) {
214
    final Path filePath = getPath();
215
216
    return filePath == null ? false : filePath.equals( check );
217
  }
218
219
  /**
220
   * Reads the entire file contents from the path associated with this tab.
221
   */
222
  private void load() {
223
    final Path filePath = getPath();
224
225
    if( filePath != null ) {
226
      try {
227
        getEditorPane().setText( asString( Files.readAllBytes( filePath ) ) );
228
      } catch( Exception ex ) {
229
        alert(
230
          "FileEditor.loadFailed.title", "FileEditor.loadFailed.message", ex
231
        );
232
      }
233
    }
234
  }
235
236
  /**
237
   * Saves the entire file contents from the path associated with this tab.
238
   *
239
   * @return true The file has been saved.
240
   */
241
  public boolean save() {
242
    try {
243
      Files.write( getPath(), asBytes( getEditorPane().getText() ) );
244
      getEditorPane().getUndoManager().mark();
245
      return true;
246
    } catch( Exception ex ) {
247
      return alert(
248
        "FileEditor.saveFailed.title", "FileEditor.saveFailed.message", ex
249
      );
250
    }
251
  }
252
253
  /**
254
   * Creates an alert dialog and waits for it to close.
255
   *
256
   * @param titleKey Resource bundle key for the alert dialog title.
257
   * @param messageKey Resource bundle key for the alert dialog message.
258
   * @param e The unexpected happening.
259
   *
260
   * @return false
261
   */
262
  private boolean alert(
263
    final String titleKey, final String messageKey, final Exception e ) {
264
    final AlertService service = getAlertService();
265
266
    final AlertMessage message = service.createAlertMessage(
267
      Messages.get( titleKey ),
268
      Messages.get( messageKey ),
269
      getPath(),
270
      e.getMessage()
271
    );
272
273
    service.createAlertError( message ).showAndWait();
274
    return false;
275
  }
276
277
  /**
278
   * Returns a best guess at the file encoding. If the encoding could not be
279
   * detected, this will return the default charset for the JVM.
280
   *
281
   * @param bytes The bytes to perform character encoding detection.
282
   *
283
   * @return The character encoding.
284
   */
285
  private Charset detectEncoding( final byte[] bytes ) {
286
    final UniversalDetector detector = new UniversalDetector( null );
287
    detector.handleData( bytes, 0, bytes.length );
288
    detector.dataEnd();
289
290
    final String charset = detector.getDetectedCharset();
291
    final Charset charEncoding = charset == null
292
      ? Charset.defaultCharset()
293
      : Charset.forName( charset.toUpperCase( ENGLISH ) );
294
295
    detector.reset();
296
297
    return charEncoding;
298
  }
299
300
  /**
301
   * Converts the given string to an array of bytes using the encoding that was
302
   * originally detected (if any) and associated with this file.
303
   *
304
   * @param text The text to convert into the original file encoding.
305
   *
306
   * @return A series of bytes ready for writing to a file.
307
   */
308
  private byte[] asBytes( final String text ) {
309
    return text.getBytes( getEncoding() );
310
  }
311
312
  /**
313
   * Converts the given bytes into a Java String. This will call setEncoding
314
   * with the encoding detected by the CharsetDetector.
315
   *
316
   * @param text The text of unknown character encoding.
317
   *
318
   * @return The text, in its auto-detected encoding, as a String.
319
   */
320
  private String asString( final byte[] text ) {
321
    setEncoding( detectEncoding( text ) );
322
    return new String( text, getEncoding() );
323
  }
324
325
  public Path getPath() {
326
    return this.path;
327
  }
328
329
  void setPath( final Path path ) {
330
    this.path = path;
331
  }
332
333
  public boolean isModified() {
334
    return this.modified.get();
335
  }
336
337
  ReadOnlyBooleanProperty modifiedProperty() {
338
    return this.modified.getReadOnlyProperty();
339
  }
340
341
  BooleanProperty canUndoProperty() {
342
    return this.canUndo;
343
  }
344
345
  BooleanProperty canRedoProperty() {
346
    return this.canRedo;
347
  }
348
349
  private UndoManager getUndoManager() {
350
    return getEditorPane().getUndoManager();
351
  }
352
353
  /**
354
   * Forwards the request to the editor pane.
355
   *
356
   * @param <T> The type of event listener to add.
357
   * @param <U> The type of consumer to add.
358
   * @param event The event that should trigger updates to the listener.
359
   * @param consumer The listener to receive update events.
360
   */
361
  public <T extends Event, U extends T> void addEventListener(
362
    final EventPattern<? super T, ? extends U> event,
363
    final Consumer<? super U> consumer ) {
364
    getEditorPane().addEventListener( event, consumer );
365
  }
366
367
  /**
368
   * Forwards to the editor pane's listeners for keyboard events.
369
   *
370
   * @param map The new input map to replace the existing keyboard listener.
371
   */
372
  public void addEventListener( final InputMap<InputEvent> map ) {
373
    getEditorPane().addEventListener( map );
374
  }
375
376
  /**
377
   * Forwards to the editor pane's listeners for keyboard events.
378
   *
379
   * @param map The existing input map to remove from the keyboard listeners.
380
   */
381
  public void removeEventListener( final InputMap<InputEvent> map ) {
382
    getEditorPane().removeEventListener( map );
383
  }
384
385
  /**
386
   * Forwards to the editor pane's listeners for text change events.
387
   *
388
   * @param listener The listener to notify when the text changes.
389
   */
390
  public void addTextChangeListener( final ChangeListener<String> listener ) {
391
    getEditorPane().addTextChangeListener( listener );
392
  }
393
394
  /**
395
   * Forwards to the editor pane's listeners for caret paragraph change events.
396
   *
397
   * @param listener The listener to notify when the caret changes paragraphs.
398
   */
399
  public void addCaretParagraphListener( final ChangeListener<Integer> listener ) {
400
    getEditorPane().addCaretParagraphListener( listener );
401
  }
402
403
  /**
404
   * Forwards the request to the editor pane.
405
   *
406
   * @return The text to process.
407
   */
408
  public String getEditorText() {
409
    return getEditorPane().getText();
410
  }
411
412
  /**
413
   * Returns the editor pane, or creates one if it doesn't yet exist.
414
   *
415
   * @return The editor pane, never null.
416
   */
417
  public EditorPane getEditorPane() {
418
    if( this.editorPane == null ) {
419
      this.editorPane = new MarkdownEditorPane();
420
    }
421
422
    return this.editorPane;
423
  }
424
425
  private AlertService getAlertService() {
30
import com.scrivenvar.service.events.Notification;
31
import com.scrivenvar.service.events.NotifyService;
32
import java.nio.charset.Charset;
33
import java.nio.file.Files;
34
import java.nio.file.Path;
35
import static java.util.Locale.ENGLISH;
36
import java.util.function.Consumer;
37
import javafx.application.Platform;
38
import javafx.beans.binding.Bindings;
39
import javafx.beans.property.BooleanProperty;
40
import javafx.beans.property.ReadOnlyBooleanProperty;
41
import javafx.beans.property.ReadOnlyBooleanWrapper;
42
import javafx.beans.property.SimpleBooleanProperty;
43
import javafx.beans.value.ChangeListener;
44
import javafx.beans.value.ObservableValue;
45
import javafx.event.Event;
46
import javafx.scene.Node;
47
import javafx.scene.control.Tab;
48
import javafx.scene.control.Tooltip;
49
import javafx.scene.input.InputEvent;
50
import javafx.scene.text.Text;
51
import org.fxmisc.richtext.StyleClassedTextArea;
52
import org.fxmisc.undo.UndoManager;
53
import org.fxmisc.wellbehaved.event.EventPattern;
54
import org.fxmisc.wellbehaved.event.InputMap;
55
import org.mozilla.universalchardet.UniversalDetector;
56
57
/**
58
 * Editor for a single file.
59
 *
60
 * @author Karl Tauber and White Magic Software, Ltd.
61
 */
62
public final class FileEditorTab extends Tab {
63
64
  private final NotifyService alertService = Services.load( NotifyService.class );
65
  private EditorPane editorPane;
66
67
  /**
68
   * Character encoding used by the file (or default encoding if none found).
69
   */
70
  private Charset encoding;
71
72
  private final ReadOnlyBooleanWrapper modified = new ReadOnlyBooleanWrapper();
73
  private final BooleanProperty canUndo = new SimpleBooleanProperty();
74
  private final BooleanProperty canRedo = new SimpleBooleanProperty();
75
76
  // Might be simpler to revert this back to a property and have the main
77
  // window listen for changes to it...
78
  private Path path;
79
80
  FileEditorTab( final Path path ) {
81
    setPath( path );
82
83
    this.modified.addListener( (observable, oldPath, newPath) -> updateTab() );
84
    updateTab();
85
86
    setOnSelectionChanged( e -> {
87
      if( isSelected() ) {
88
        Platform.runLater( () -> activated() );
89
      }
90
    } );
91
  }
92
93
  private void updateTab() {
94
    setText( getTabTitle() );
95
    setGraphic( getModifiedMark() );
96
    setTooltip( getTabTooltip() );
97
  }
98
99
  /**
100
   * Returns the base filename (without the directory names).
101
   *
102
   * @return The untitled text if the path hasn't been set.
103
   */
104
  private String getTabTitle() {
105
    final Path filePath = getPath();
106
107
    return (filePath == null)
108
      ? Messages.get( "FileEditor.untitled" )
109
      : filePath.getFileName().toString();
110
  }
111
112
  /**
113
   * Returns the full filename represented by the path.
114
   *
115
   * @return The untitled text if the path hasn't been set.
116
   */
117
  private Tooltip getTabTooltip() {
118
    final Path filePath = getPath();
119
    return new Tooltip( filePath == null ? "" : filePath.toString() );
120
  }
121
122
  /**
123
   * Returns a marker to indicate whether the file has been modified.
124
   *
125
   * @return "*" when the file has changed; otherwise null.
126
   */
127
  private Text getModifiedMark() {
128
    return isModified() ? new Text( "*" ) : null;
129
  }
130
131
  /**
132
   * Called when the user switches tab.
133
   */
134
  private void activated() {
135
    // Tab is closed or no longer active.
136
    if( getTabPane() == null || !isSelected() ) {
137
      return;
138
    }
139
140
    // Switch to the tab without loading if the contents are already in memory.
141
    if( getContent() != null ) {
142
      getEditorPane().requestFocus();
143
      return;
144
    }
145
146
    // Load the text and update the preview before the undo manager.
147
    load();
148
149
    // Track undo requests -- can only be called *after* load.
150
    initUndoManager();
151
    initLayout();
152
    initFocus();
153
  }
154
155
  private void initLayout() {
156
    setContent( getScrollPane() );
157
  }
158
159
  private Node getScrollPane() {
160
    return getEditorPane().getScrollPane();
161
  }
162
163
  private void initFocus() {
164
    getEditorPane().requestFocus();
165
  }
166
167
  private void initUndoManager() {
168
    final UndoManager undoManager = getUndoManager();
169
170
    // Clear undo history after first load.
171
    undoManager.forgetHistory();
172
173
    // Bind the editor undo manager to the properties.
174
    modified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
175
    canUndo.bind( undoManager.undoAvailableProperty() );
176
    canRedo.bind( undoManager.redoAvailableProperty() );
177
  }
178
179
  /**
180
   * Returns the index into the text where the caret blinks happily away.
181
   *
182
   * @return A number from 0 to the editor's document text length.
183
   */
184
  public int getCaretPosition() {
185
    return getEditor().getCaretPosition();
186
  }
187
188
  /**
189
   * Allows observers to synchronize caret position changes.
190
   *
191
   * @return An observable caret property value.
192
   */
193
  public final ObservableValue<Integer> caretPositionProperty() {
194
    return getEditor().caretPositionProperty();
195
  }
196
197
  /**
198
   * Returns the text area associated with this tab.
199
   *
200
   * @return A text editor.
201
   */
202
  private StyleClassedTextArea getEditor() {
203
    return getEditorPane().getEditor();
204
  }
205
206
  /**
207
   * Returns true if the given path exactly matches this tab's path.
208
   *
209
   * @param check The path to compare against.
210
   *
211
   * @return true The paths are the same.
212
   */
213
  public boolean isPath( final Path check ) {
214
    final Path filePath = getPath();
215
216
    return filePath == null ? false : filePath.equals( check );
217
  }
218
219
  /**
220
   * Reads the entire file contents from the path associated with this tab.
221
   */
222
  private void load() {
223
    final Path filePath = getPath();
224
225
    if( filePath != null ) {
226
      try {
227
        getEditorPane().setText( asString( Files.readAllBytes( filePath ) ) );
228
      } catch( Exception ex ) {
229
        alert(
230
          "FileEditor.loadFailed.title", "FileEditor.loadFailed.message", ex
231
        );
232
      }
233
    }
234
  }
235
236
  /**
237
   * Saves the entire file contents from the path associated with this tab.
238
   *
239
   * @return true The file has been saved.
240
   */
241
  public boolean save() {
242
    try {
243
      Files.write( getPath(), asBytes( getEditorPane().getText() ) );
244
      getEditorPane().getUndoManager().mark();
245
      return true;
246
    } catch( Exception ex ) {
247
      return alert(
248
        "FileEditor.saveFailed.title", "FileEditor.saveFailed.message", ex
249
      );
250
    }
251
  }
252
253
  /**
254
   * Creates an alert dialog and waits for it to close.
255
   *
256
   * @param titleKey Resource bundle key for the alert dialog title.
257
   * @param messageKey Resource bundle key for the alert dialog message.
258
   * @param e The unexpected happening.
259
   *
260
   * @return false
261
   */
262
  private boolean alert(
263
    final String titleKey, final String messageKey, final Exception e ) {
264
    final NotifyService service = getAlertService();
265
    final Path filePath = getPath();
266
267
    final Notification message = service.createNotification(
268
      Messages.get( titleKey ),
269
      Messages.get( messageKey ),
270
      filePath == null ? "" : filePath,
271
      e.getMessage()
272
    );
273
274
    service.createError( message ).showAndWait();
275
    return false;
276
  }
277
278
  /**
279
   * Returns a best guess at the file encoding. If the encoding could not be
280
   * detected, this will return the default charset for the JVM.
281
   *
282
   * @param bytes The bytes to perform character encoding detection.
283
   *
284
   * @return The character encoding.
285
   */
286
  private Charset detectEncoding( final byte[] bytes ) {
287
    final UniversalDetector detector = new UniversalDetector( null );
288
    detector.handleData( bytes, 0, bytes.length );
289
    detector.dataEnd();
290
291
    final String charset = detector.getDetectedCharset();
292
    final Charset charEncoding = charset == null
293
      ? Charset.defaultCharset()
294
      : Charset.forName( charset.toUpperCase( ENGLISH ) );
295
296
    detector.reset();
297
298
    return charEncoding;
299
  }
300
301
  /**
302
   * Converts the given string to an array of bytes using the encoding that was
303
   * originally detected (if any) and associated with this file.
304
   *
305
   * @param text The text to convert into the original file encoding.
306
   *
307
   * @return A series of bytes ready for writing to a file.
308
   */
309
  private byte[] asBytes( final String text ) {
310
    return text.getBytes( getEncoding() );
311
  }
312
313
  /**
314
   * Converts the given bytes into a Java String. This will call setEncoding
315
   * with the encoding detected by the CharsetDetector.
316
   *
317
   * @param text The text of unknown character encoding.
318
   *
319
   * @return The text, in its auto-detected encoding, as a String.
320
   */
321
  private String asString( final byte[] text ) {
322
    setEncoding( detectEncoding( text ) );
323
    return new String( text, getEncoding() );
324
  }
325
326
  public Path getPath() {
327
    return this.path;
328
  }
329
330
  public void setPath( final Path path ) {
331
    this.path = path;
332
  }
333
334
  /**
335
   * Answers whether this tab has an initialized path reference.
336
   *
337
   * @return false This tab has no path.
338
   */
339
  public boolean isFileOpen() {
340
    return this.path != null;
341
  }
342
343
  public boolean isModified() {
344
    return this.modified.get();
345
  }
346
347
  ReadOnlyBooleanProperty modifiedProperty() {
348
    return this.modified.getReadOnlyProperty();
349
  }
350
351
  BooleanProperty canUndoProperty() {
352
    return this.canUndo;
353
  }
354
355
  BooleanProperty canRedoProperty() {
356
    return this.canRedo;
357
  }
358
359
  private UndoManager getUndoManager() {
360
    return getEditorPane().getUndoManager();
361
  }
362
363
  /**
364
   * Forwards the request to the editor pane.
365
   *
366
   * @param <T> The type of event listener to add.
367
   * @param <U> The type of consumer to add.
368
   * @param event The event that should trigger updates to the listener.
369
   * @param consumer The listener to receive update events.
370
   */
371
  public <T extends Event, U extends T> void addEventListener(
372
    final EventPattern<? super T, ? extends U> event,
373
    final Consumer<? super U> consumer ) {
374
    getEditorPane().addEventListener( event, consumer );
375
  }
376
377
  /**
378
   * Forwards to the editor pane's listeners for keyboard events.
379
   *
380
   * @param map The new input map to replace the existing keyboard listener.
381
   */
382
  public void addEventListener( final InputMap<InputEvent> map ) {
383
    getEditorPane().addEventListener( map );
384
  }
385
386
  /**
387
   * Forwards to the editor pane's listeners for keyboard events.
388
   *
389
   * @param map The existing input map to remove from the keyboard listeners.
390
   */
391
  public void removeEventListener( final InputMap<InputEvent> map ) {
392
    getEditorPane().removeEventListener( map );
393
  }
394
395
  /**
396
   * Forwards to the editor pane's listeners for text change events.
397
   *
398
   * @param listener The listener to notify when the text changes.
399
   */
400
  public void addTextChangeListener( final ChangeListener<String> listener ) {
401
    getEditorPane().addTextChangeListener( listener );
402
  }
403
404
  /**
405
   * Forwards to the editor pane's listeners for caret paragraph change events.
406
   *
407
   * @param listener The listener to notify when the caret changes paragraphs.
408
   */
409
  public void addCaretParagraphListener( final ChangeListener<Integer> listener ) {
410
    getEditorPane().addCaretParagraphListener( listener );
411
  }
412
413
  /**
414
   * Forwards the request to the editor pane.
415
   *
416
   * @return The text to process.
417
   */
418
  public String getEditorText() {
419
    return getEditorPane().getText();
420
  }
421
422
  /**
423
   * Returns the editor pane, or creates one if it doesn't yet exist.
424
   *
425
   * @return The editor pane, never null.
426
   */
427
  public EditorPane getEditorPane() {
428
    if( this.editorPane == null ) {
429
      this.editorPane = new MarkdownEditorPane();
430
    }
431
432
    return this.editorPane;
433
  }
434
435
  private NotifyService getAlertService() {
426436
    return this.alertService;
427437
  }
M src/main/java/com/scrivenvar/FileEditorTabPane.java
3434
import com.scrivenvar.service.Options;
3535
import com.scrivenvar.service.Settings;
36
import com.scrivenvar.service.events.AlertMessage;
37
import com.scrivenvar.service.events.AlertService;
38
import static com.scrivenvar.service.events.AlertService.NO;
39
import static com.scrivenvar.service.events.AlertService.YES;
40
import com.scrivenvar.util.Utils;
41
import java.io.File;
42
import java.nio.file.Path;
43
import java.util.ArrayList;
44
import java.util.List;
45
import java.util.function.Consumer;
46
import java.util.prefs.Preferences;
47
import java.util.stream.Collectors;
48
import javafx.beans.property.ReadOnlyBooleanProperty;
49
import javafx.beans.property.ReadOnlyBooleanWrapper;
50
import javafx.beans.property.ReadOnlyObjectProperty;
51
import javafx.beans.property.ReadOnlyObjectWrapper;
52
import javafx.beans.value.ChangeListener;
53
import javafx.beans.value.ObservableValue;
54
import javafx.collections.ListChangeListener;
55
import javafx.collections.ObservableList;
56
import javafx.event.Event;
57
import javafx.scene.Node;
58
import javafx.scene.control.Alert;
59
import javafx.scene.control.ButtonType;
60
import javafx.scene.control.Tab;
61
import javafx.scene.control.TabPane;
62
import javafx.scene.control.TabPane.TabClosingPolicy;
63
import javafx.scene.input.InputEvent;
64
import javafx.stage.FileChooser;
65
import javafx.stage.FileChooser.ExtensionFilter;
66
import javafx.stage.Window;
67
import org.fxmisc.richtext.StyledTextArea;
68
import org.fxmisc.wellbehaved.event.EventPattern;
69
import org.fxmisc.wellbehaved.event.InputMap;
70
71
/**
72
 * Tab pane for file editors.
73
 *
74
 * @author Karl Tauber and White Magic Software, Ltd.
75
 */
76
public final class FileEditorTabPane extends TabPane {
77
78
  private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose.filter";
79
80
  private final Options options = Services.load( Options.class );
81
  private final Settings settings = Services.load( Settings.class );
82
  private final AlertService alertService = Services.load( AlertService.class );
83
84
  private final ReadOnlyObjectWrapper<Path> openDefinition = new ReadOnlyObjectWrapper<>();
85
  private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>();
86
  private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper();
87
88
  /**
89
   * Constructs a new file editor tab pane.
90
   */
91
  public FileEditorTabPane() {
92
    final ObservableList<Tab> tabs = getTabs();
93
94
    setFocusTraversable( false );
95
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
96
97
    addTabSelectionListener(
98
      (ObservableValue<? extends Tab> tabPane,
99
        final Tab oldTab, final Tab newTab) -> {
100
101
        if( newTab != null ) {
102
          activeFileEditor.set( (FileEditorTab)newTab );
103
        }
104
      }
105
    );
106
107
    final ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> {
108
      for( final Tab tab : tabs ) {
109
        if( ((FileEditorTab)tab).isModified() ) {
110
          this.anyFileEditorModified.set( true );
111
          break;
112
        }
113
      }
114
    };
115
116
    tabs.addListener(
117
      (ListChangeListener<Tab>)change -> {
118
        while( change.next() ) {
119
          if( change.wasAdded() ) {
120
            change.getAddedSubList().stream().forEach( (tab) -> {
121
              ((FileEditorTab)tab).modifiedProperty().addListener( modifiedListener );
122
            } );
123
          } else if( change.wasRemoved() ) {
124
            change.getRemoved().stream().forEach( (tab) -> {
125
              ((FileEditorTab)tab).modifiedProperty().removeListener( modifiedListener );
126
            } );
127
          }
128
        }
129
130
        // Changes in the tabs may also change anyFileEditorModified property
131
        // (e.g. closed modified file)
132
        modifiedListener.changed( null, null, null );
133
      }
134
    );
135
  }
136
137
  /**
138
   * Delegates to the active file editor.
139
   *
140
   * @param <T> Event type.
141
   * @param <U> Consumer type.
142
   * @param event Event to pass to the editor.
143
   * @param consumer Consumer to pass to the editor.
144
   */
145
  public <T extends Event, U extends T> void addEventListener(
146
    final EventPattern<? super T, ? extends U> event,
147
    final Consumer<? super U> consumer ) {
148
    getActiveFileEditor().addEventListener( event, consumer );
149
  }
150
151
  /**
152
   * Delegates to the active file editor pane, and, ultimately, to its text
153
   * area.
154
   *
155
   * @param map The map of methods to events.
156
   */
157
  public void addEventListener( final InputMap<InputEvent> map ) {
158
    getActiveFileEditor().addEventListener( map );
159
  }
160
161
  /**
162
   * Remove a keyboard event listener from the active file editor.
163
   *
164
   * @param map The keyboard events to remove.
165
   */
166
  public void removeEventListener( final InputMap<InputEvent> map ) {
167
    getActiveFileEditor().removeEventListener( map );
168
  }
169
170
  /**
171
   * Allows observers to be notified when the current file editor tab changes.
172
   *
173
   * @param listener The listener to notify of tab change events.
174
   */
175
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
176
    // Observe the tab so that when a new tab is opened or selected,
177
    // a notification is kicked off.
178
    getSelectionModel().selectedItemProperty().addListener( listener );
179
  }
180
181
  /**
182
   * Allows clients to manipulate the editor content directly.
183
   *
184
   * @return The text area for the active file editor.
185
   */
186
  public StyledTextArea getEditor() {
187
    return getActiveFileEditor().getEditorPane().getEditor();
188
  }
189
190
  public FileEditorTab getActiveFileEditor() {
191
    return this.activeFileEditor.get();
192
  }
193
194
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
195
    return this.activeFileEditor.getReadOnlyProperty();
196
  }
197
198
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
199
    return this.anyFileEditorModified.getReadOnlyProperty();
200
  }
201
202
  private FileEditorTab createFileEditor( final Path path ) {
203
    final FileEditorTab tab = new FileEditorTab( path );
204
205
    tab.setOnCloseRequest( e -> {
206
      if( !canCloseEditor( tab ) ) {
207
        e.consume();
208
      }
209
    } );
210
211
    return tab;
212
  }
213
214
  /**
215
   * Called when the user selects New from the File menu.
216
   *
217
   * @return The newly added tab.
218
   */
219
  void newEditor() {
220
    final FileEditorTab tab = createFileEditor( null );
221
222
    getTabs().add( tab );
223
    getSelectionModel().select( tab );
224
  }
225
226
  void openFileDialog() {
227
    final String title = get( "Dialog.file.choose.open.title" );
228
    final FileChooser dialog = createFileChooser( title );
229
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
230
231
    if( files != null ) {
232
      openFiles( files );
233
    }
234
  }
235
236
  /**
237
   * Opens the files into new editors, unless one of those files was a
238
   * definition file. The definition file is loaded into the definition pane,
239
   * but only the first one selected (multiple definition files will result in a
240
   * warning).
241
   *
242
   * @param files The list of non-definition files that the were requested to
243
   * open.
244
   *
245
   * @return A list of files that can be opened in text editors.
246
   */
247
  private void openFiles( final List<File> files ) {
248
    final FileTypePredicate predicate
249
      = new FileTypePredicate( createExtensionFilter( DEFINITION ).getExtensions() );
250
251
    // The user might have opened multiple definitions files. These will
252
    // be discarded from the text editable files.
253
    final List<File> definitions
254
      = files.stream().filter( predicate ).collect( Collectors.toList() );
255
256
    // Create a modifiable list to remove any definition files that were
257
    // opened.
258
    final List<File> editors = new ArrayList<>( files );
259
260
    if( editors.size() > 0 ) {
261
      saveLastDirectory( editors.get( 0 ) );
262
    }
263
264
    editors.removeAll( definitions );
265
266
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
267
    if( editors.size() > 0 ) {
268
      openEditors( editors, 0 );
269
    }
270
271
    if( definitions.size() > 0 ) {
272
      openDefinition( definitions.get( 0 ) );
273
    }
274
  }
275
276
  private void openEditors( final List<File> files, final int activeIndex ) {
277
    final int fileTally = files.size();
278
    final List<Tab> tabs = getTabs();
279
280
    // Close single unmodified "Untitled" tab.
281
    if( tabs.size() == 1 ) {
282
      final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 ));
283
284
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
285
        closeEditor( fileEditor, false );
286
      }
287
    }
288
289
    for( int i = 0; i < fileTally; i++ ) {
290
      final Path path = files.get( i ).toPath();
291
292
      FileEditorTab fileEditorTab = findEditor( path );
293
294
      // Only open new files.
295
      if( fileEditorTab == null ) {
296
        fileEditorTab = createFileEditor( path );
297
        getTabs().add( fileEditorTab );
298
      }
299
300
      // Select the first file in the list.
301
      if( i == activeIndex ) {
302
        getSelectionModel().select( fileEditorTab );
303
      }
304
    }
305
  }
306
307
  /**
308
   * Returns a property that changes when a new definition file is opened.
309
   *
310
   * @return The path to a definition file that was opened.
311
   */
312
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
313
    return getOnOpenDefinitionFile().getReadOnlyProperty();
314
  }
315
316
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
317
    return this.openDefinition;
318
  }
319
320
  /**
321
   * Called when the user has opened a definition file (using the file open
322
   * dialog box). This will replace the current set of definitions for the
323
   * active tab.
324
   *
325
   * @param definition The file to open.
326
   */
327
  private void openDefinition( final File definition ) {
328
    // TODO: Prevent reading this file twice when a new text document is opened.
329
    // (might be a matter of checking the value first).
330
    getOnOpenDefinitionFile().set( definition.toPath() );
331
  }
332
333
  boolean saveEditor( final FileEditorTab fileEditor ) {
334
    if( fileEditor == null || !fileEditor.isModified() ) {
335
      return true;
336
    }
337
338
    if( fileEditor.getPath() == null ) {
339
      getSelectionModel().select( fileEditor );
340
341
      final FileChooser fileChooser = createFileChooser( Messages.get( "Dialog.file.choose.save.title" ) );
342
      final File file = fileChooser.showSaveDialog( getWindow() );
343
      if( file == null ) {
344
        return false;
345
      }
346
347
      saveLastDirectory( file );
348
      fileEditor.setPath( file.toPath() );
349
    }
350
351
    return fileEditor.save();
352
  }
353
354
  boolean saveAllEditors() {
355
    boolean success = true;
356
357
    for( FileEditorTab fileEditor : getAllEditors() ) {
358
      if( !saveEditor( fileEditor ) ) {
359
        success = false;
360
      }
361
    }
362
363
    return success;
364
  }
365
366
  /**
367
   * Answers whether the file has had modifications. '
368
   *
369
   * @param tab THe tab to check for modifications.
370
   *
371
   * @return false The file is unmodified.
372
   */
373
  boolean canCloseEditor( final FileEditorTab tab ) {
374
    if( !tab.isModified() ) {
375
      return true;
376
    }
377
378
    final AlertMessage message = getAlertService().createAlertMessage(
379
      Messages.get( "Alert.file.close.title" ),
380
      Messages.get( "Alert.file.close.text" ),
381
      tab.getText()
382
    );
383
384
    final Alert alert = getAlertService().createAlertConfirmation( message );
385
    final ButtonType response = alert.showAndWait().get();
386
387
    return response == YES ? saveEditor( tab ) : response == NO;
388
  }
389
390
  private AlertService getAlertService() {
36
import com.scrivenvar.service.events.Notification;
37
import com.scrivenvar.service.events.NotifyService;
38
import static com.scrivenvar.service.events.NotifyService.NO;
39
import static com.scrivenvar.service.events.NotifyService.YES;
40
import com.scrivenvar.util.Utils;
41
import java.io.File;
42
import java.nio.file.Path;
43
import java.util.ArrayList;
44
import java.util.List;
45
import java.util.function.Consumer;
46
import java.util.prefs.Preferences;
47
import java.util.stream.Collectors;
48
import javafx.beans.property.ReadOnlyBooleanProperty;
49
import javafx.beans.property.ReadOnlyBooleanWrapper;
50
import javafx.beans.property.ReadOnlyObjectProperty;
51
import javafx.beans.property.ReadOnlyObjectWrapper;
52
import javafx.beans.value.ChangeListener;
53
import javafx.beans.value.ObservableValue;
54
import javafx.collections.ListChangeListener;
55
import javafx.collections.ObservableList;
56
import javafx.event.Event;
57
import javafx.scene.Node;
58
import javafx.scene.control.Alert;
59
import javafx.scene.control.ButtonType;
60
import javafx.scene.control.Tab;
61
import javafx.scene.control.TabPane;
62
import javafx.scene.control.TabPane.TabClosingPolicy;
63
import javafx.scene.input.InputEvent;
64
import javafx.stage.FileChooser;
65
import javafx.stage.FileChooser.ExtensionFilter;
66
import javafx.stage.Window;
67
import org.fxmisc.richtext.StyledTextArea;
68
import org.fxmisc.wellbehaved.event.EventPattern;
69
import org.fxmisc.wellbehaved.event.InputMap;
70
71
/**
72
 * Tab pane for file editors.
73
 *
74
 * @author Karl Tauber and White Magic Software, Ltd.
75
 */
76
public final class FileEditorTabPane extends TabPane {
77
78
  private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose.filter";
79
80
  private final Options options = Services.load( Options.class );
81
  private final Settings settings = Services.load( Settings.class );
82
  private final NotifyService alertService = Services.load(NotifyService.class );
83
84
  private final ReadOnlyObjectWrapper<Path> openDefinition = new ReadOnlyObjectWrapper<>();
85
  private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>();
86
  private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper();
87
88
  /**
89
   * Constructs a new file editor tab pane.
90
   */
91
  public FileEditorTabPane() {
92
    final ObservableList<Tab> tabs = getTabs();
93
94
    setFocusTraversable( false );
95
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
96
97
    addTabSelectionListener(
98
      (ObservableValue<? extends Tab> tabPane,
99
        final Tab oldTab, final Tab newTab) -> {
100
101
        if( newTab != null ) {
102
          activeFileEditor.set( (FileEditorTab)newTab );
103
        }
104
      }
105
    );
106
107
    final ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> {
108
      for( final Tab tab : tabs ) {
109
        if( ((FileEditorTab)tab).isModified() ) {
110
          this.anyFileEditorModified.set( true );
111
          break;
112
        }
113
      }
114
    };
115
116
    tabs.addListener(
117
      (ListChangeListener<Tab>)change -> {
118
        while( change.next() ) {
119
          if( change.wasAdded() ) {
120
            change.getAddedSubList().stream().forEach( (tab) -> {
121
              ((FileEditorTab)tab).modifiedProperty().addListener( modifiedListener );
122
            } );
123
          } else if( change.wasRemoved() ) {
124
            change.getRemoved().stream().forEach( (tab) -> {
125
              ((FileEditorTab)tab).modifiedProperty().removeListener( modifiedListener );
126
            } );
127
          }
128
        }
129
130
        // Changes in the tabs may also change anyFileEditorModified property
131
        // (e.g. closed modified file)
132
        modifiedListener.changed( null, null, null );
133
      }
134
    );
135
  }
136
137
  /**
138
   * Delegates to the active file editor.
139
   *
140
   * @param <T> Event type.
141
   * @param <U> Consumer type.
142
   * @param event Event to pass to the editor.
143
   * @param consumer Consumer to pass to the editor.
144
   */
145
  public <T extends Event, U extends T> void addEventListener(
146
    final EventPattern<? super T, ? extends U> event,
147
    final Consumer<? super U> consumer ) {
148
    getActiveFileEditor().addEventListener( event, consumer );
149
  }
150
151
  /**
152
   * Delegates to the active file editor pane, and, ultimately, to its text
153
   * area.
154
   *
155
   * @param map The map of methods to events.
156
   */
157
  public void addEventListener( final InputMap<InputEvent> map ) {
158
    getActiveFileEditor().addEventListener( map );
159
  }
160
161
  /**
162
   * Remove a keyboard event listener from the active file editor.
163
   *
164
   * @param map The keyboard events to remove.
165
   */
166
  public void removeEventListener( final InputMap<InputEvent> map ) {
167
    getActiveFileEditor().removeEventListener( map );
168
  }
169
170
  /**
171
   * Allows observers to be notified when the current file editor tab changes.
172
   *
173
   * @param listener The listener to notify of tab change events.
174
   */
175
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
176
    // Observe the tab so that when a new tab is opened or selected,
177
    // a notification is kicked off.
178
    getSelectionModel().selectedItemProperty().addListener( listener );
179
  }
180
181
  /**
182
   * Allows clients to manipulate the editor content directly.
183
   *
184
   * @return The text area for the active file editor.
185
   */
186
  public StyledTextArea getEditor() {
187
    return getActiveFileEditor().getEditorPane().getEditor();
188
  }
189
190
  public FileEditorTab getActiveFileEditor() {
191
    return this.activeFileEditor.get();
192
  }
193
194
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
195
    return this.activeFileEditor.getReadOnlyProperty();
196
  }
197
198
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
199
    return this.anyFileEditorModified.getReadOnlyProperty();
200
  }
201
202
  private FileEditorTab createFileEditor( final Path path ) {
203
    final FileEditorTab tab = new FileEditorTab( path );
204
205
    tab.setOnCloseRequest( e -> {
206
      if( !canCloseEditor( tab ) ) {
207
        e.consume();
208
      }
209
    } );
210
211
    return tab;
212
  }
213
214
  /**
215
   * Called when the user selects New from the File menu.
216
   *
217
   * @return The newly added tab.
218
   */
219
  void newEditor() {
220
    final FileEditorTab tab = createFileEditor( null );
221
222
    getTabs().add( tab );
223
    getSelectionModel().select( tab );
224
  }
225
226
  void openFileDialog() {
227
    final String title = get( "Dialog.file.choose.open.title" );
228
    final FileChooser dialog = createFileChooser( title );
229
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
230
231
    if( files != null ) {
232
      openFiles( files );
233
    }
234
  }
235
236
  /**
237
   * Opens the files into new editors, unless one of those files was a
238
   * definition file. The definition file is loaded into the definition pane,
239
   * but only the first one selected (multiple definition files will result in a
240
   * warning).
241
   *
242
   * @param files The list of non-definition files that the were requested to
243
   * open.
244
   *
245
   * @return A list of files that can be opened in text editors.
246
   */
247
  private void openFiles( final List<File> files ) {
248
    final FileTypePredicate predicate
249
      = new FileTypePredicate( createExtensionFilter( DEFINITION ).getExtensions() );
250
251
    // The user might have opened multiple definitions files. These will
252
    // be discarded from the text editable files.
253
    final List<File> definitions
254
      = files.stream().filter( predicate ).collect( Collectors.toList() );
255
256
    // Create a modifiable list to remove any definition files that were
257
    // opened.
258
    final List<File> editors = new ArrayList<>( files );
259
260
    if( editors.size() > 0 ) {
261
      saveLastDirectory( editors.get( 0 ) );
262
    }
263
264
    editors.removeAll( definitions );
265
266
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
267
    if( editors.size() > 0 ) {
268
      openEditors( editors, 0 );
269
    }
270
271
    if( definitions.size() > 0 ) {
272
      openDefinition( definitions.get( 0 ) );
273
    }
274
  }
275
276
  private void openEditors( final List<File> files, final int activeIndex ) {
277
    final int fileTally = files.size();
278
    final List<Tab> tabs = getTabs();
279
280
    // Close single unmodified "Untitled" tab.
281
    if( tabs.size() == 1 ) {
282
      final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 ));
283
284
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
285
        closeEditor( fileEditor, false );
286
      }
287
    }
288
289
    for( int i = 0; i < fileTally; i++ ) {
290
      final Path path = files.get( i ).toPath();
291
292
      FileEditorTab fileEditorTab = findEditor( path );
293
294
      // Only open new files.
295
      if( fileEditorTab == null ) {
296
        fileEditorTab = createFileEditor( path );
297
        getTabs().add( fileEditorTab );
298
      }
299
300
      // Select the first file in the list.
301
      if( i == activeIndex ) {
302
        getSelectionModel().select( fileEditorTab );
303
      }
304
    }
305
  }
306
307
  /**
308
   * Returns a property that changes when a new definition file is opened.
309
   *
310
   * @return The path to a definition file that was opened.
311
   */
312
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
313
    return getOnOpenDefinitionFile().getReadOnlyProperty();
314
  }
315
316
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
317
    return this.openDefinition;
318
  }
319
320
  /**
321
   * Called when the user has opened a definition file (using the file open
322
   * dialog box). This will replace the current set of definitions for the
323
   * active tab.
324
   *
325
   * @param definition The file to open.
326
   */
327
  private void openDefinition( final File definition ) {
328
    // TODO: Prevent reading this file twice when a new text document is opened.
329
    // (might be a matter of checking the value first).
330
    getOnOpenDefinitionFile().set( definition.toPath() );
331
  }
332
333
  boolean saveEditor( final FileEditorTab fileEditor ) {
334
    if( fileEditor == null || !fileEditor.isModified() ) {
335
      return true;
336
    }
337
338
    if( fileEditor.getPath() == null ) {
339
      getSelectionModel().select( fileEditor );
340
341
      final FileChooser fileChooser = createFileChooser( Messages.get( "Dialog.file.choose.save.title" ) );
342
      final File file = fileChooser.showSaveDialog( getWindow() );
343
      if( file == null ) {
344
        return false;
345
      }
346
347
      saveLastDirectory( file );
348
      fileEditor.setPath( file.toPath() );
349
    }
350
351
    return fileEditor.save();
352
  }
353
354
  boolean saveAllEditors() {
355
    boolean success = true;
356
357
    for( FileEditorTab fileEditor : getAllEditors() ) {
358
      if( !saveEditor( fileEditor ) ) {
359
        success = false;
360
      }
361
    }
362
363
    return success;
364
  }
365
366
  /**
367
   * Answers whether the file has had modifications. '
368
   *
369
   * @param tab THe tab to check for modifications.
370
   *
371
   * @return false The file is unmodified.
372
   */
373
  boolean canCloseEditor( final FileEditorTab tab ) {
374
    if( !tab.isModified() ) {
375
      return true;
376
    }
377
378
    final Notification message = getAlertService().createNotification(
379
      Messages.get( "Alert.file.close.title" ),
380
      Messages.get( "Alert.file.close.text" ),
381
      tab.getText()
382
    );
383
384
    final Alert alert = getAlertService().createConfirmation( message );
385
    final ButtonType response = alert.showAndWait().get();
386
387
    return response == YES ? saveEditor( tab ) : response == NO;
388
  }
389
390
  private NotifyService getAlertService() {
391391
    return this.alertService;
392392
  }
M src/main/java/com/scrivenvar/Main.java
2929
3030
import static com.scrivenvar.Constants.*;
31
import com.scrivenvar.preferences.FilePreferencesFactory;
3132
import com.scrivenvar.service.Options;
3233
import com.scrivenvar.service.Snitch;
33
import com.scrivenvar.service.events.AlertService;
3434
import com.scrivenvar.util.StageState;
3535
import javafx.application.Application;
3636
import javafx.scene.Scene;
3737
import javafx.scene.image.Image;
3838
import javafx.stage.Stage;
39
import com.scrivenvar.service.events.NotifyService;
3940
4041
/**
4142
 * Main application entry point. The application allows users to edit Markdown
4243
 * files and see a real-time preview of the edits.
4344
 *
4445
 * @author Karl Tauber and White Magic Software, Ltd.
4546
 */
4647
public final class Main extends Application {
4748
48
  private final Options options = Services.load( Options.class );
49
  private final Snitch snitch = Services.load( Snitch.class );
49
  private Options options;
50
  private Snitch snitch;
5051
  private Thread snitchThread;
5152
5253
  private static Application app;
5354
  private final MainWindow mainWindow = new MainWindow();
5455
5556
  public static void main( final String[] args ) {
57
    initPreferences();
5658
    launch( args );
59
  }
60
61
  /**
62
   * Sets the factory used for reading user preferences.
63
   */
64
  private static void initPreferences() {
65
    System.setProperty(
66
      "java.util.prefs.PreferencesFactory",
67
      FilePreferencesFactory.class.getName()
68
    );
5769
  }
5870
...
99111
100112
  private void initAlertService() {
101
    final AlertService service = Services.load( AlertService.class );
113
    final NotifyService service = Services.load(NotifyService.class );
102114
    service.setWindow( getScene().getWindow() );
103115
  }
...
125137
  }
126138
127
  private Snitch getWatchDog() {
139
  private synchronized Snitch getWatchDog() {
140
    if( this.snitch == null ) {
141
      this.snitch = Services.load( Snitch.class );
142
    }
143
    
128144
    return this.snitch;
129145
  }
...
137153
  }
138154
139
  private Options getOptions() {
155
  private synchronized Options getOptions() {
156
    if( this.options == null ) {
157
      this.options = Services.load( Options.class );
158
    }
159
140160
    return this.options;
141161
  }
M src/main/java/com/scrivenvar/MainWindow.java
8888
 */
8989
public class MainWindow implements Observer {
90
  
91
  private final Options options = Services.load( Options.class );
92
  private final Snitch snitch = Services.load( Snitch.class );
93
  
94
  private Scene scene;
95
  private MenuBar menuBar;
96
  
97
  private DefinitionSource definitionSource;
98
  private DefinitionPane definitionPane;
99
  private FileEditorTabPane fileEditorPane;
100
  private HTMLPreviewPane previewPane;
101
102
  /**
103
   * Prevent re-instantiation processing classes.
104
   */
105
  private Map<FileEditorTab, Processor<String>> processors;
106
107
  public MainWindow() {
108
    initLayout();
109
    initOpenDefinitionListener();
110
    initTabAddedListener();
111
    initTabChangedListener();
112
    initPreferences();
113
    initWatchDog();
114
  }
115
116
  /**
117
   * Listen for file editor tab pane to receive an open definition source event.
118
   */
119
  private void initOpenDefinitionListener() {
120
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
121
      (ObservableValue<? extends Path> definitionFile,
122
        final Path oldPath, final Path newPath) -> {
123
        openDefinition( newPath );
124
        setProcessors( null );
125
        refreshSelectedTab( getActiveFileEditor() );
126
      }
127
    );
128
  }
129
130
  /**
131
   * When tabs are added, hook the various change listeners onto the new tab so
132
   * that the preview pane refreshes as necessary.
133
   */
134
  private void initTabAddedListener() {
135
    final FileEditorTabPane editorPane = getFileEditorPane();
136
137
    // Make sure the text processor kicks off when new files are opened.
138
    final ObservableList<Tab> tabs = editorPane.getTabs();
139
140
    // Update the preview pane on tab changes.
141
    tabs.addListener(
142
      (final Change<? extends Tab> change) -> {
143
        while( change.next() ) {
144
          if( change.wasAdded() ) {
145
            // Multiple tabs can be added simultaneously.
146
            for( final Tab newTab : change.getAddedSubList() ) {
147
              final FileEditorTab tab = (FileEditorTab)newTab;
148
              
149
              initTextChangeListener( tab );
150
              initCaretParagraphListener( tab );
151
              initVariableNameInjector( tab );
152
            }
153
          }
154
        }
155
      }
156
    );
157
  }
158
159
  /**
160
   * Reloads the preferences from the previous load.
161
   */
162
  private void initPreferences() {
163
    restoreDefinitionSource();
164
    getFileEditorPane().restorePreferences();
165
    updateDefinitionPane();
166
  }
167
168
  /**
169
   * Listen for new tab selection events.
170
   */
171
  private void initTabChangedListener() {
172
    final FileEditorTabPane editorPane = getFileEditorPane();
173
174
    // Update the preview pane changing tabs.
175
    editorPane.addTabSelectionListener(
176
      (ObservableValue<? extends Tab> tabPane,
177
        final Tab oldTab, final Tab newTab) -> {
178
179
        // If there was no old tab, then this is a first time load, which
180
        // can be ignored.
181
        if( oldTab != null ) {
182
          if( newTab == null ) {
183
            closeRemainingTab();
184
          } else {
185
            // Update the preview with the edited text.
186
            refreshSelectedTab( (FileEditorTab)newTab );
187
          }
188
        }
189
      }
190
    );
191
  }
192
  
193
  private void initTextChangeListener( final FileEditorTab tab ) {
194
    tab.addTextChangeListener(
195
      (ObservableValue<? extends String> editor,
196
        final String oldValue, final String newValue) -> {
197
        refreshSelectedTab( tab );
198
      }
199
    );
200
  }
201
  
202
  private void initCaretParagraphListener( final FileEditorTab tab ) {
203
    tab.addCaretParagraphListener(
204
      (ObservableValue<? extends Integer> editor,
205
        final Integer oldValue, final Integer newValue) -> {
206
        refreshSelectedTab( tab );
207
      }
208
    );
209
  }
210
  
211
  private void initVariableNameInjector( final FileEditorTab tab ) {
212
    VariableNameInjector.listen( tab, getDefinitionPane() );
213
  }
214
215
  /**
216
   * Watch for changes to external files. In particular, this awaits
217
   * modifications to any XSL files associated with XML files being edited. When
218
   * an XSL file is modified (external to the application), the watchdog's ears
219
   * perk up and the file is reloaded. This keeps the XSL transformation up to
220
   * date with what's on the file system.
221
   */
222
  private void initWatchDog() {
223
    getSnitch().addObserver( this );
224
  }
225
226
  /**
227
   * Called whenever the preview pane becomes out of sync with the file editor
228
   * tab. This can be called when the text changes, the caret paragraph changes,
229
   * or the file tab changes.
230
   *
231
   * @param tab The file editor tab that has been changed in some fashion.
232
   */
233
  private void refreshSelectedTab( final FileEditorTab tab ) {
234
    getPreviewPane().setPath( tab.getPath() );
235
    
236
    Processor<String> processor = getProcessors().get( tab );
237
    
238
    if( processor == null ) {
239
      processor = createProcessor( tab );
240
      getProcessors().put( tab, processor );
241
    }
242
    
243
    processor.processChain( tab.getEditorText() );
244
  }
245
246
  /**
247
   * Returns the variable map of interpolated definitions.
248
   *
249
   * @return A map to help dereference variables.
250
   */
251
  private Map<String, String> getResolvedMap() {
252
    return getDefinitionSource().getResolvedMap();
253
  }
254
255
  /**
256
   * Returns the root node for the hierarchical definition source.
257
   *
258
   * @return Data to display in the definition pane.
259
   */
260
  private TreeView<String> getTreeView() {
261
    try {
262
      return getDefinitionSource().asTreeView();
263
    } catch( Exception e ) {
264
      alert( e );
265
    }
266
    
267
    return new TreeView<>();
268
  }
269
270
  /**
271
   * Called when a definition file is opened.
272
   *
273
   * @param path Path to the file that was opened.
274
   */
275
  private void openDefinition( final Path path ) {
276
    openDefinition( path.toString() );
277
  }
278
279
  /**
280
   * Called to load a definition file from its source location.
281
   *
282
   * @param path The path to the definition file that was loaded.
283
   */
284
  private void openDefinition( final String path ) {
285
    try {
286
      final DefinitionSource ds = createDefinitionSource( path );
287
      setDefinitionSource( ds );
288
      storeDefinitionSource();
289
      updateDefinitionPane();
290
    } catch( Exception e ) {
291
      alert( e );
292
    }
293
  }
294
  
295
  private void updateDefinitionPane() {
296
    getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
297
  }
298
  
299
  private void restoreDefinitionSource() {
300
    final Preferences preferences = getPreferences();
301
    final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
302
    setDefinitionSource( createDefinitionSource( source ) );
303
  }
304
  
305
  private void storeDefinitionSource() {
306
    final Preferences preferences = getPreferences();
307
    final DefinitionSource ds = getDefinitionSource();
308
    
309
    preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
310
  }
311
312
  /**
313
   * Called when the last open tab is closed. This clears out the preview pane
314
   * and the definition pane.
315
   */
316
  private void closeRemainingTab() {
317
    getPreviewPane().clear();
318
    getDefinitionPane().clear();
319
  }
320
321
  /**
322
   * Called when an exception occurs that warrants the user's attention.
323
   *
324
   * @param e The exception with a message that the user should know about.
325
   */
326
  private void alert( final Exception e ) {
327
    // TODO: Update the status bar.
328
  }
329
330
  //---- File actions -------------------------------------------------------
331
  /**
332
   * Called when a file has been modified.
333
   *
334
   * @param snitch The watchdog file monitoring instance.
335
   * @param file The file that was modified.
336
   */
337
  @Override
338
  public void update( final Observable snitch, final Object file ) {
339
    if( file instanceof Path ) {
340
      update( (Path)file );
341
    }
342
  }
343
344
  /**
345
   * Called when a file has been modified.
346
   *
347
   * @param file Path to the modified file.
348
   */
349
  private void update( final Path file ) {
350
    // Avoid throwing IllegalStateException by running from a non-JavaFX thread.
351
    Platform.runLater(
352
      () -> {
353
        // Brute-force XSLT file reload by re-instantiating all processors.
354
        resetProcessors();
355
        refreshSelectedTab( getActiveFileEditor() );
356
      }
357
    );
358
  }
359
360
  /**
361
   * After resetting the processors, they will refresh anew to be up-to-date
362
   * with the files (text and definition) currently loaded into the editor.
363
   */
364
  private void resetProcessors() {
365
    getProcessors().clear();
366
  }
367
368
  //---- File actions -------------------------------------------------------
369
  private void fileNew() {
370
    getFileEditorPane().newEditor();
371
  }
372
  
373
  private void fileOpen() {
374
    getFileEditorPane().openFileDialog();
375
  }
376
  
377
  private void fileClose() {
378
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
379
  }
380
  
381
  private void fileCloseAll() {
382
    getFileEditorPane().closeAllEditors();
383
  }
384
  
385
  private void fileSave() {
386
    getFileEditorPane().saveEditor( getActiveFileEditor() );
387
  }
388
  
389
  private void fileSaveAll() {
390
    getFileEditorPane().saveAllEditors();
391
  }
392
  
393
  private void fileExit() {
394
    final Window window = getWindow();
395
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
396
  }
397
398
  //---- Help actions -------------------------------------------------------
399
  private void helpAbout() {
400
    Alert alert = new Alert( AlertType.INFORMATION );
401
    alert.setTitle( get( "Dialog.about.title" ) );
402
    alert.setHeaderText( get( "Dialog.about.header" ) );
403
    alert.setContentText( get( "Dialog.about.content" ) );
404
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
405
    alert.initOwner( getWindow() );
406
    
407
    alert.showAndWait();
408
  }
409
410
  //---- Convenience accessors ----------------------------------------------
411
  private float getFloat( final String key, final float defaultValue ) {
412
    return getPreferences().getFloat( key, defaultValue );
413
  }
414
  
415
  private Preferences getPreferences() {
416
    return getOptions().getState();
417
  }
418
  
419
  private Window getWindow() {
420
    return getScene().getWindow();
421
  }
422
  
423
  private MarkdownEditorPane getActiveEditor() {
424
    final EditorPane pane = getActiveFileEditor().getEditorPane();
425
    
426
    return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
427
  }
428
  
429
  private FileEditorTab getActiveFileEditor() {
430
    return getFileEditorPane().getActiveFileEditor();
431
  }
432
433
  //---- Member accessors ---------------------------------------------------
434
  private void setScene( Scene scene ) {
435
    this.scene = scene;
436
  }
437
  
438
  public Scene getScene() {
439
    return this.scene;
440
  }
441
  
442
  private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) {
443
    this.processors = map;
444
  }
445
  
446
  private Map<FileEditorTab, Processor<String>> getProcessors() {
447
    if( this.processors == null ) {
448
      setProcessors( new HashMap<>() );
449
    }
450
    
451
    return this.processors;
452
  }
453
454
  private FileEditorTabPane getFileEditorPane() {
455
    if( this.fileEditorPane == null ) {
456
      this.fileEditorPane = createFileEditorPane();
457
    }
458
    
459
    return this.fileEditorPane;
460
  }
461
  
462
  private HTMLPreviewPane getPreviewPane() {
463
    if( this.previewPane == null ) {
464
      this.previewPane = createPreviewPane();
465
    }
466
    
467
    return this.previewPane;
468
  }
469
  
470
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
471
    this.definitionSource = definitionSource;
472
  }
473
  
474
  private DefinitionSource getDefinitionSource() {
475
    if( this.definitionSource == null ) {
476
      this.definitionSource = new EmptyDefinitionSource();
477
    }
478
    
479
    return this.definitionSource;
480
  }
481
  
482
  private DefinitionPane getDefinitionPane() {
483
    if( this.definitionPane == null ) {
484
      this.definitionPane = createDefinitionPane();
485
    }
486
    
487
    return this.definitionPane;
488
  }
489
  
490
  private Options getOptions() {
491
    return this.options;
492
  }
493
  
494
  private Snitch getSnitch() {
495
    return this.snitch;
496
  }
497
  
498
  public void setMenuBar( MenuBar menuBar ) {
499
    this.menuBar = menuBar;
500
  }
501
  
502
  public MenuBar getMenuBar() {
503
    return this.menuBar;
504
  }
505
506
  //---- Member creators ----------------------------------------------------
507
  /**
508
   * Factory to create processors that are suited to different file types.
509
   *
510
   * @param tab The tab that is subjected to processing.
511
   *
512
   * @return A processor suited to the file type specified by the tab's path.
513
   */
514
  private Processor<String> createProcessor( final FileEditorTab tab ) {
515
    return createProcessorFactory().createProcessor( tab );
516
  }
517
  
518
  private ProcessorFactory createProcessorFactory() {
519
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
520
  }
521
  
522
  private DefinitionSource createDefinitionSource( final String path ) {
523
    return createDefinitionFactory().createDefinitionSource( path );
524
  }
525
526
  /**
527
   * Create an editor pane to hold file editor tabs.
528
   *
529
   * @return A new instance, never null.
530
   */
531
  private FileEditorTabPane createFileEditorPane() {
532
    return new FileEditorTabPane();
533
  }
534
  
535
  private HTMLPreviewPane createPreviewPane() {
536
    return new HTMLPreviewPane();
537
  }
538
  
539
  private DefinitionPane createDefinitionPane() {
540
    return new DefinitionPane( getTreeView() );
541
  }
542
  
543
  private DefinitionFactory createDefinitionFactory() {
544
    return new DefinitionFactory();
545
  }
546
  
547
  private Node createMenuBar() {
548
    final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
549
550
    // File actions
551
    Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
552
    Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
553
    Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
554
    Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
555
    Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
556
      createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
557
    Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
558
      Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
559
    Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
560
561
    // Edit actions
562
    Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
563
      e -> getActiveEditor().undo(),
564
      createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
565
    Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
566
      e -> getActiveEditor().redo(),
567
      createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
568
569
    // Insert actions
570
    Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
571
      e -> getActiveEditor().surroundSelection( "**", "**" ),
572
      activeFileEditorIsNull );
573
    Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
574
      e -> getActiveEditor().surroundSelection( "*", "*" ),
575
      activeFileEditorIsNull );
576
    Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
577
      e -> getActiveEditor().surroundSelection( "~~", "~~" ),
578
      activeFileEditorIsNull );
579
    Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
580
      e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
581
      activeFileEditorIsNull );
582
    Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
583
      e -> getActiveEditor().surroundSelection( "`", "`" ),
584
      activeFileEditorIsNull );
585
    Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
586
      e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ),
587
      activeFileEditorIsNull );
588
    
589
    Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
590
      e -> getActiveEditor().insertLink(),
591
      activeFileEditorIsNull );
592
    Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT,
593
      e -> getActiveEditor().insertImage(),
594
      activeFileEditorIsNull );
595
    
596
    final Action[] headers = new Action[ 6 ];
597
598
    // Insert header actions (H1 ... H6)
599
    for( int i = 1; i <= 6; i++ ) {
600
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
601
      final String markup = String.format( "%n%n%s ", hashes );
602
      final String text = get( "Main.menu.insert.header_" + i );
603
      final String accelerator = "Shortcut+" + i;
604
      final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
605
      
606
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
607
        e -> getActiveEditor().surroundSelection( markup, "", prompt ),
608
        activeFileEditorIsNull );
609
    }
610
    
611
    Action insertUnorderedListAction = new Action( get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
612
      e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
613
      activeFileEditorIsNull );
614
    Action insertOrderedListAction = new Action( get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
615
      e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
616
      activeFileEditorIsNull );
617
    Action insertHorizontalRuleAction = new Action( get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
618
      e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
619
      activeFileEditorIsNull );
620
621
    // Help actions
622
    Action helpAboutAction = new Action( get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
623
624
    //---- MenuBar ----
625
    Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ),
626
      fileNewAction,
627
      fileOpenAction,
628
      null,
629
      fileCloseAction,
630
      fileCloseAllAction,
631
      null,
632
      fileSaveAction,
633
      fileSaveAllAction,
634
      null,
635
      fileExitAction );
636
    
637
    Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ),
638
      editUndoAction,
639
      editRedoAction );
640
    
641
    Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ),
642
      insertBoldAction,
643
      insertItalicAction,
644
      insertStrikethroughAction,
645
      insertBlockquoteAction,
646
      insertCodeAction,
647
      insertFencedCodeBlockAction,
648
      null,
649
      insertLinkAction,
650
      insertImageAction,
651
      null,
652
      headers[ 0 ],
653
      headers[ 1 ],
654
      headers[ 2 ],
655
      headers[ 3 ],
656
      headers[ 4 ],
657
      headers[ 5 ],
658
      null,
659
      insertUnorderedListAction,
660
      insertOrderedListAction,
661
      insertHorizontalRuleAction );
662
    
663
    Menu helpMenu = ActionUtils.createMenu( get( "Main.menu.help" ),
664
      helpAboutAction );
665
    
666
    menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu );
667
668
    //---- ToolBar ----
669
    ToolBar toolBar = ActionUtils.createToolBar(
670
      fileNewAction,
671
      fileOpenAction,
672
      fileSaveAction,
673
      null,
674
      editUndoAction,
675
      editRedoAction,
676
      null,
677
      insertBoldAction,
678
      insertItalicAction,
679
      insertBlockquoteAction,
680
      insertCodeAction,
681
      insertFencedCodeBlockAction,
682
      null,
683
      insertLinkAction,
684
      insertImageAction,
685
      null,
686
      headers[ 0 ],
687
      null,
688
      insertUnorderedListAction,
689
      insertOrderedListAction );
690
    
691
    return new VBox( menuBar, toolBar );
692
  }
693
694
  /**
695
   * Creates a boolean property that is bound to another boolean value of the
696
   * active editor.
697
   */
698
  private BooleanProperty createActiveBooleanProperty(
699
    final Function<FileEditorTab, ObservableBooleanValue> func ) {
700
    
701
    final BooleanProperty b = new SimpleBooleanProperty();
702
    final FileEditorTab tab = getActiveFileEditor();
703
    
704
    if( tab != null ) {
705
      b.bind( func.apply( tab ) );
706
    }
707
    
708
    getFileEditorPane().activeFileEditorProperty().addListener(
709
      (observable, oldFileEditor, newFileEditor) -> {
710
        b.unbind();
711
        
712
        if( newFileEditor != null ) {
713
          b.bind( func.apply( newFileEditor ) );
714
        } else {
715
          b.set( false );
716
        }
717
      }
718
    );
719
    
720
    return b;
721
  }
722
  
723
  private void initLayout() {
724
    final SplitPane splitPane = new SplitPane(
725
      getDefinitionPane().getNode(),
726
      getFileEditorPane().getNode(),
727
      getPreviewPane().getNode() );
728
    
729
    splitPane.setDividerPositions(
730
      getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
731
      getFloat( K_PANE_SPLIT_EDITOR, .45f ),
732
      getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
733
734
    // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
735
    final BorderPane borderPane = new BorderPane();
736
    borderPane.setPrefSize( 1024, 800 );
737
    borderPane.setTop( createMenuBar() );
738
    borderPane.setCenter( splitPane );
739
    
90
91
  private final Options options = Services.load( Options.class );
92
  private final Snitch snitch = Services.load( Snitch.class );
93
94
  private Scene scene;
95
  private MenuBar menuBar;
96
97
  private DefinitionSource definitionSource;
98
  private DefinitionPane definitionPane;
99
  private FileEditorTabPane fileEditorPane;
100
  private HTMLPreviewPane previewPane;
101
102
  /**
103
   * Prevent re-instantiation processing classes.
104
   */
105
  private Map<FileEditorTab, Processor<String>> processors;
106
107
  public MainWindow() {
108
    initLayout();
109
    initDefinitionListener();
110
    initTabAddedListener();
111
    initTabChangedListener();
112
    initPreferences();
113
    initWatchDog();
114
  }
115
116
  /**
117
   * Listen for file editor tab pane to receive an open definition source event.
118
   */
119
  private void initDefinitionListener() {
120
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
121
      (ObservableValue<? extends Path> definitionFile,
122
        final Path oldPath, final Path newPath) -> {
123
        openDefinition( newPath );
124
125
        // Indirectly refresh the resolved map.
126
        setProcessors( null );
127
        
128
        // Will create new processors and therefore a new resolved map.
129
        refreshSelectedTab( getActiveFileEditor() );
130
        
131
        updateDefinitionPane();
132
      }
133
    );
134
  }
135
136
  /**
137
   * When tabs are added, hook the various change listeners onto the new tab so
138
   * that the preview pane refreshes as necessary.
139
   */
140
  private void initTabAddedListener() {
141
    final FileEditorTabPane editorPane = getFileEditorPane();
142
143
    // Make sure the text processor kicks off when new files are opened.
144
    final ObservableList<Tab> tabs = editorPane.getTabs();
145
146
    // Update the preview pane on tab changes.
147
    tabs.addListener(
148
      (final Change<? extends Tab> change) -> {
149
        while( change.next() ) {
150
          if( change.wasAdded() ) {
151
            // Multiple tabs can be added simultaneously.
152
            for( final Tab newTab : change.getAddedSubList() ) {
153
              final FileEditorTab tab = (FileEditorTab)newTab;
154
155
              initTextChangeListener( tab );
156
              initCaretParagraphListener( tab );
157
              initVariableNameInjector( tab );
158
            }
159
          }
160
        }
161
      }
162
    );
163
  }
164
165
  /**
166
   * Reloads the preferences from the previous load.
167
   */
168
  private void initPreferences() {
169
    restoreDefinitionSource();
170
    getFileEditorPane().restorePreferences();
171
    updateDefinitionPane();
172
  }
173
174
  /**
175
   * Listen for new tab selection events.
176
   */
177
  private void initTabChangedListener() {
178
    final FileEditorTabPane editorPane = getFileEditorPane();
179
180
    // Update the preview pane changing tabs.
181
    editorPane.addTabSelectionListener(
182
      (ObservableValue<? extends Tab> tabPane,
183
        final Tab oldTab, final Tab newTab) -> {
184
185
        // If there was no old tab, then this is a first time load, which
186
        // can be ignored.
187
        if( oldTab != null ) {
188
          if( newTab == null ) {
189
            closeRemainingTab();
190
          } else {
191
            // Update the preview with the edited text.
192
            refreshSelectedTab( (FileEditorTab)newTab );
193
          }
194
        }
195
      }
196
    );
197
  }
198
199
  private void initTextChangeListener( final FileEditorTab tab ) {
200
    tab.addTextChangeListener(
201
      (ObservableValue<? extends String> editor,
202
        final String oldValue, final String newValue) -> {
203
        refreshSelectedTab( tab );
204
      }
205
    );
206
  }
207
208
  private void initCaretParagraphListener( final FileEditorTab tab ) {
209
    tab.addCaretParagraphListener(
210
      (ObservableValue<? extends Integer> editor,
211
        final Integer oldValue, final Integer newValue) -> {
212
        refreshSelectedTab( tab );
213
      }
214
    );
215
  }
216
217
  private void initVariableNameInjector( final FileEditorTab tab ) {
218
    VariableNameInjector.listen( tab, getDefinitionPane() );
219
  }
220
221
  /**
222
   * Watch for changes to external files. In particular, this awaits
223
   * modifications to any XSL files associated with XML files being edited. When
224
   * an XSL file is modified (external to the application), the watchdog's ears
225
   * perk up and the file is reloaded. This keeps the XSL transformation up to
226
   * date with what's on the file system.
227
   */
228
  private void initWatchDog() {
229
    getSnitch().addObserver( this );
230
  }
231
232
  /**
233
   * Called whenever the preview pane becomes out of sync with the file editor
234
   * tab. This can be called when the text changes, the caret paragraph changes,
235
   * or the file tab changes.
236
   *
237
   * @param tab The file editor tab that has been changed in some fashion.
238
   */
239
  private void refreshSelectedTab( final FileEditorTab tab ) {
240
    if( tab.isFileOpen() ) {
241
      getPreviewPane().setPath( tab.getPath() );
242
243
      Processor<String> processor = getProcessors().get( tab );
244
245
      if( processor == null ) {
246
        processor = createProcessor( tab );
247
        getProcessors().put( tab, processor );
248
      }
249
250
      processor.processChain( tab.getEditorText() );
251
    }
252
  }
253
254
  /**
255
   * Returns the variable map of interpolated definitions.
256
   *
257
   * @return A map to help dereference variables.
258
   */
259
  private Map<String, String> getResolvedMap() {
260
    return getDefinitionSource().getResolvedMap();
261
  }
262
263
  /**
264
   * Returns the root node for the hierarchical definition source.
265
   *
266
   * @return Data to display in the definition pane.
267
   */
268
  private TreeView<String> getTreeView() {
269
    try {
270
      return getDefinitionSource().asTreeView();
271
    } catch( Exception e ) {
272
      alert( e );
273
    }
274
275
    return new TreeView<>();
276
  }
277
278
  /**
279
   * Called when a definition source is opened.
280
   *
281
   * @param path Path to the definition source that was opened.
282
   */
283
  private void openDefinition( final Path path ) {
284
    try {
285
      final DefinitionSource ds = createDefinitionSource( path.toString() );
286
      setDefinitionSource( ds );
287
      storeDefinitionSource();
288
      updateDefinitionPane();
289
    } catch( final Exception e ) {
290
      alert( e );
291
    }
292
  }
293
294
  private void updateDefinitionPane() {
295
    getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
296
  }
297
298
  private void restoreDefinitionSource() {
299
    final Preferences preferences = getPreferences();
300
    final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
301
302
    // If there's no definition source set, don't try to load it.
303
    if( source != null ) {
304
      setDefinitionSource( createDefinitionSource( source ) );
305
    }
306
  }
307
308
  private void storeDefinitionSource() {
309
    final Preferences preferences = getPreferences();
310
    final DefinitionSource ds = getDefinitionSource();
311
312
    preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
313
  }
314
315
  /**
316
   * Called when the last open tab is closed to clear the preview pane.
317
   */
318
  private void closeRemainingTab() {
319
    getPreviewPane().clear();
320
  }
321
322
  /**
323
   * Called when an exception occurs that warrants the user's attention.
324
   *
325
   * @param e The exception with a message that the user should know about.
326
   */
327
  private void alert( final Exception e ) {
328
    // TODO: Update the status bar.
329
  }
330
331
  //---- File actions -------------------------------------------------------
332
  /**
333
   * Called when a file has been modified.
334
   *
335
   * @param snitch The watchdog file monitoring instance.
336
   * @param file The file that was modified.
337
   */
338
  @Override
339
  public void update( final Observable snitch, final Object file ) {
340
    if( file instanceof Path ) {
341
      update( (Path)file );
342
    }
343
  }
344
345
  /**
346
   * Called when a file has been modified.
347
   *
348
   * @param file Path to the modified file.
349
   */
350
  private void update( final Path file ) {
351
    // Avoid throwing IllegalStateException by running from a non-JavaFX thread.
352
    Platform.runLater(
353
      () -> {
354
        // Brute-force XSLT file reload by re-instantiating all processors.
355
        resetProcessors();
356
        refreshSelectedTab( getActiveFileEditor() );
357
      }
358
    );
359
  }
360
361
  /**
362
   * After resetting the processors, they will refresh anew to be up-to-date
363
   * with the files (text and definition) currently loaded into the editor.
364
   */
365
  private void resetProcessors() {
366
    getProcessors().clear();
367
  }
368
369
  //---- File actions -------------------------------------------------------
370
  private void fileNew() {
371
    getFileEditorPane().newEditor();
372
  }
373
374
  private void fileOpen() {
375
    getFileEditorPane().openFileDialog();
376
  }
377
378
  private void fileClose() {
379
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
380
  }
381
382
  private void fileCloseAll() {
383
    getFileEditorPane().closeAllEditors();
384
  }
385
386
  private void fileSave() {
387
    getFileEditorPane().saveEditor( getActiveFileEditor() );
388
  }
389
390
  private void fileSaveAll() {
391
    getFileEditorPane().saveAllEditors();
392
  }
393
394
  private void fileExit() {
395
    final Window window = getWindow();
396
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
397
  }
398
399
  //---- Help actions -------------------------------------------------------
400
  private void helpAbout() {
401
    Alert alert = new Alert( AlertType.INFORMATION );
402
    alert.setTitle( get( "Dialog.about.title" ) );
403
    alert.setHeaderText( get( "Dialog.about.header" ) );
404
    alert.setContentText( get( "Dialog.about.content" ) );
405
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
406
    alert.initOwner( getWindow() );
407
408
    alert.showAndWait();
409
  }
410
411
  //---- Convenience accessors ----------------------------------------------
412
  private float getFloat( final String key, final float defaultValue ) {
413
    return getPreferences().getFloat( key, defaultValue );
414
  }
415
416
  private Preferences getPreferences() {
417
    return getOptions().getState();
418
  }
419
420
  private Window getWindow() {
421
    return getScene().getWindow();
422
  }
423
424
  private MarkdownEditorPane getActiveEditor() {
425
    final EditorPane pane = getActiveFileEditor().getEditorPane();
426
427
    return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
428
  }
429
430
  private FileEditorTab getActiveFileEditor() {
431
    return getFileEditorPane().getActiveFileEditor();
432
  }
433
434
  //---- Member accessors ---------------------------------------------------
435
  private void setScene( Scene scene ) {
436
    this.scene = scene;
437
  }
438
439
  public Scene getScene() {
440
    return this.scene;
441
  }
442
443
  private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) {
444
    this.processors = map;
445
  }
446
447
  private Map<FileEditorTab, Processor<String>> getProcessors() {
448
    if( this.processors == null ) {
449
      setProcessors( new HashMap<>() );
450
    }
451
452
    return this.processors;
453
  }
454
455
  private FileEditorTabPane getFileEditorPane() {
456
    if( this.fileEditorPane == null ) {
457
      this.fileEditorPane = createFileEditorPane();
458
    }
459
460
    return this.fileEditorPane;
461
  }
462
463
  private HTMLPreviewPane getPreviewPane() {
464
    if( this.previewPane == null ) {
465
      this.previewPane = createPreviewPane();
466
    }
467
468
    return this.previewPane;
469
  }
470
471
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
472
    this.definitionSource = definitionSource;
473
  }
474
475
  private DefinitionSource getDefinitionSource() {
476
    if( this.definitionSource == null ) {
477
      this.definitionSource = new EmptyDefinitionSource();
478
    }
479
480
    return this.definitionSource;
481
  }
482
483
  private DefinitionPane getDefinitionPane() {
484
    if( this.definitionPane == null ) {
485
      this.definitionPane = createDefinitionPane();
486
    }
487
488
    return this.definitionPane;
489
  }
490
491
  private Options getOptions() {
492
    return this.options;
493
  }
494
495
  private Snitch getSnitch() {
496
    return this.snitch;
497
  }
498
499
  public void setMenuBar( MenuBar menuBar ) {
500
    this.menuBar = menuBar;
501
  }
502
503
  public MenuBar getMenuBar() {
504
    return this.menuBar;
505
  }
506
507
  //---- Member creators ----------------------------------------------------
508
  /**
509
   * Factory to create processors that are suited to different file types.
510
   *
511
   * @param tab The tab that is subjected to processing.
512
   *
513
   * @return A processor suited to the file type specified by the tab's path.
514
   */
515
  private Processor<String> createProcessor( final FileEditorTab tab ) {
516
    return createProcessorFactory().createProcessor( tab );
517
  }
518
519
  private ProcessorFactory createProcessorFactory() {
520
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
521
  }
522
523
  private DefinitionSource createDefinitionSource( final String path ) {
524
    return createDefinitionFactory().createDefinitionSource( path );
525
  }
526
527
  /**
528
   * Create an editor pane to hold file editor tabs.
529
   *
530
   * @return A new instance, never null.
531
   */
532
  private FileEditorTabPane createFileEditorPane() {
533
    return new FileEditorTabPane();
534
  }
535
536
  private HTMLPreviewPane createPreviewPane() {
537
    return new HTMLPreviewPane();
538
  }
539
540
  private DefinitionPane createDefinitionPane() {
541
    return new DefinitionPane( getTreeView() );
542
  }
543
544
  private DefinitionFactory createDefinitionFactory() {
545
    return new DefinitionFactory();
546
  }
547
548
  private Node createMenuBar() {
549
    final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
550
551
    // File actions
552
    Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
553
    Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
554
    Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
555
    Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
556
    Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
557
      createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
558
    Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
559
      Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
560
    Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
561
562
    // Edit actions
563
    Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
564
      e -> getActiveEditor().undo(),
565
      createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
566
    Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
567
      e -> getActiveEditor().redo(),
568
      createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
569
570
    // Insert actions
571
    Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
572
      e -> getActiveEditor().surroundSelection( "**", "**" ),
573
      activeFileEditorIsNull );
574
    Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
575
      e -> getActiveEditor().surroundSelection( "*", "*" ),
576
      activeFileEditorIsNull );
577
    Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
578
      e -> getActiveEditor().surroundSelection( "~~", "~~" ),
579
      activeFileEditorIsNull );
580
    Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
581
      e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
582
      activeFileEditorIsNull );
583
    Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
584
      e -> getActiveEditor().surroundSelection( "`", "`" ),
585
      activeFileEditorIsNull );
586
    Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
587
      e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ),
588
      activeFileEditorIsNull );
589
590
    Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
591
      e -> getActiveEditor().insertLink(),
592
      activeFileEditorIsNull );
593
    Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT,
594
      e -> getActiveEditor().insertImage(),
595
      activeFileEditorIsNull );
596
597
    final Action[] headers = new Action[ 6 ];
598
599
    // Insert header actions (H1 ... H6)
600
    for( int i = 1; i <= 6; i++ ) {
601
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
602
      final String markup = String.format( "%n%n%s ", hashes );
603
      final String text = get( "Main.menu.insert.header_" + i );
604
      final String accelerator = "Shortcut+" + i;
605
      final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
606
607
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
608
        e -> getActiveEditor().surroundSelection( markup, "", prompt ),
609
        activeFileEditorIsNull );
610
    }
611
612
    Action insertUnorderedListAction = new Action( get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
613
      e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
614
      activeFileEditorIsNull );
615
    Action insertOrderedListAction = new Action( get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
616
      e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
617
      activeFileEditorIsNull );
618
    Action insertHorizontalRuleAction = new Action( get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
619
      e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
620
      activeFileEditorIsNull );
621
622
    // Help actions
623
    Action helpAboutAction = new Action( get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
624
625
    //---- MenuBar ----
626
    Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ),
627
      fileNewAction,
628
      fileOpenAction,
629
      null,
630
      fileCloseAction,
631
      fileCloseAllAction,
632
      null,
633
      fileSaveAction,
634
      fileSaveAllAction,
635
      null,
636
      fileExitAction );
637
638
    Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ),
639
      editUndoAction,
640
      editRedoAction );
641
642
    Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ),
643
      insertBoldAction,
644
      insertItalicAction,
645
      insertStrikethroughAction,
646
      insertBlockquoteAction,
647
      insertCodeAction,
648
      insertFencedCodeBlockAction,
649
      null,
650
      insertLinkAction,
651
      insertImageAction,
652
      null,
653
      headers[ 0 ],
654
      headers[ 1 ],
655
      headers[ 2 ],
656
      headers[ 3 ],
657
      headers[ 4 ],
658
      headers[ 5 ],
659
      null,
660
      insertUnorderedListAction,
661
      insertOrderedListAction,
662
      insertHorizontalRuleAction );
663
664
    Menu helpMenu = ActionUtils.createMenu( get( "Main.menu.help" ),
665
      helpAboutAction );
666
667
    menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu );
668
669
    //---- ToolBar ----
670
    ToolBar toolBar = ActionUtils.createToolBar(
671
      fileNewAction,
672
      fileOpenAction,
673
      fileSaveAction,
674
      null,
675
      editUndoAction,
676
      editRedoAction,
677
      null,
678
      insertBoldAction,
679
      insertItalicAction,
680
      insertBlockquoteAction,
681
      insertCodeAction,
682
      insertFencedCodeBlockAction,
683
      null,
684
      insertLinkAction,
685
      insertImageAction,
686
      null,
687
      headers[ 0 ],
688
      null,
689
      insertUnorderedListAction,
690
      insertOrderedListAction );
691
692
    return new VBox( menuBar, toolBar );
693
  }
694
695
  /**
696
   * Creates a boolean property that is bound to another boolean value of the
697
   * active editor.
698
   */
699
  private BooleanProperty createActiveBooleanProperty(
700
    final Function<FileEditorTab, ObservableBooleanValue> func ) {
701
702
    final BooleanProperty b = new SimpleBooleanProperty();
703
    final FileEditorTab tab = getActiveFileEditor();
704
705
    if( tab != null ) {
706
      b.bind( func.apply( tab ) );
707
    }
708
709
    getFileEditorPane().activeFileEditorProperty().addListener(
710
      (observable, oldFileEditor, newFileEditor) -> {
711
        b.unbind();
712
713
        if( newFileEditor != null ) {
714
          b.bind( func.apply( newFileEditor ) );
715
        } else {
716
          b.set( false );
717
        }
718
      }
719
    );
720
721
    return b;
722
  }
723
724
  private void initLayout() {
725
    final SplitPane splitPane = new SplitPane(
726
      getDefinitionPane().getNode(),
727
      getFileEditorPane().getNode(),
728
      getPreviewPane().getNode() );
729
730
    splitPane.setDividerPositions(
731
      getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
732
      getFloat( K_PANE_SPLIT_EDITOR, .45f ),
733
      getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
734
735
    // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
736
    final BorderPane borderPane = new BorderPane();
737
    borderPane.setPrefSize( 1024, 800 );
738
    borderPane.setTop( createMenuBar() );
739
    borderPane.setCenter( splitPane );
740
740741
    final Scene appScene = new Scene( borderPane );
741742
    setScene( appScene );
M src/main/java/com/scrivenvar/decorators/RVariableDecorator.java
4545
  @Override
4646
  public String decorate( final String variableName ) {
47
    return "`r#" + variableName + "`";
47
    return "`r#x(" + variableName + ")`";
4848
  }
4949
}
A src/main/java/com/scrivenvar/definition/FileDefinitionSource.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import java.nio.file.Path;
31
32
/**
33
 * Implements common behaviour for file definition sources.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public abstract class FileDefinitionSource extends AbstractDefinitionSource {
38
39
  private Path path;
40
41
  /**
42
   * Constructs a new file definition source that can read and write data in the
43
   * hierarchical format contained within the file location specified by the
44
   * path.
45
   *
46
   * @param path Must not be null.
47
   */
48
  public FileDefinitionSource( final Path path ) {
49
    setPath( path );
50
  }
51
52
  private void setPath( final Path path ) {
53
    this.path = path;
54
  }
55
56
  protected Path getPath() {
57
    return this.path;
58
  }
59
60
  /**
61
   * Returns the path represented by this object.
62
   *
63
   * @return The
64
   */
65
  @Override
66
  public String toString() {
67
    return getPath().toString();
68
  }
69
}
170
D src/main/java/com/scrivenvar/definition/yaml/FileDefinitionSource.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition.yaml;
29
30
import com.scrivenvar.definition.AbstractDefinitionSource;
31
import java.nio.file.Path;
32
33
/**
34
 * Implements common behaviour for file definition sources.
35
 *
36
 * @author White Magic Software, Ltd.
37
 */
38
public abstract class FileDefinitionSource extends AbstractDefinitionSource {
39
40
  private Path path;
41
42
  /**
43
   * Constructs a new file definition source that can read and write data in the
44
   * hierarchical format contained within the file location specified by the
45
   * path.
46
   *
47
   * @param path Must not be null.
48
   */
49
  public FileDefinitionSource( final Path path ) {
50
    setPath( path );
51
  }
52
53
  private void setPath( final Path path ) {
54
    this.path = path;
55
  }
56
57
  protected Path getPath() {
58
    return this.path;
59
  }
60
61
  /**
62
   * Returns the path represented by this object.
63
   *
64
   * @return The
65
   */
66
  @Override
67
  public String toString() {
68
    return getPath().toString();
69
  }
70
}
711
M src/main/java/com/scrivenvar/definition/yaml/YamlFileDefinitionSource.java
2828
package com.scrivenvar.definition.yaml;
2929
30
import com.scrivenvar.definition.FileDefinitionSource;
3031
import static com.scrivenvar.Messages.get;
3132
import java.io.InputStream;
A src/main/java/com/scrivenvar/preferences/FilePreferences.java
1
/*
2
 * Copyright 2016 David Croft and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preferences;
29
30
import java.io.File;
31
import java.io.FileInputStream;
32
import java.io.FileOutputStream;
33
import java.io.IOException;
34
import java.util.ArrayList;
35
import java.util.Enumeration;
36
import java.util.List;
37
import java.util.Map;
38
import java.util.Properties;
39
import java.util.TreeMap;
40
import java.util.prefs.AbstractPreferences;
41
import java.util.prefs.BackingStoreException;
42
43
/**
44
 * Preferences implementation that stores to a user-defined file. Local file
45
 * storage is preferred over a certain operating system's monolithic trash heap
46
 * called a registry. When the OS is locked down, the default Preferences
47
 * implementation will try to write to the registry and fail due to permissions
48
 * problems. This class sidesteps the issue entirely by writing to the user's
49
 * home directory, where permissions should be a bit more lax.
50
 * 
51
 * @see http://stackoverflow.com/q/208231/59087
52
 */
53
public class FilePreferences extends AbstractPreferences {
54
55
  private Map<String, String> root = new TreeMap<>();
56
  private Map<String, FilePreferences> children = new TreeMap<>();
57
  private boolean isRemoved;
58
59
  public FilePreferences( final AbstractPreferences parent, final String name ) {
60
    super( parent, name );
61
62
    try {
63
      sync();
64
    } catch( final BackingStoreException ex ) {
65
      problem( ex );
66
    }
67
  }
68
69
  @Override
70
  protected void putSpi( final String key, final String value ) {
71
    root.put( key, value );
72
73
    try {
74
      flush();
75
    } catch( final BackingStoreException ex ) {
76
      problem( ex );
77
    }
78
  }
79
80
  @Override
81
  protected String getSpi( final String key ) {
82
    return root.get( key );
83
  }
84
85
  @Override
86
  protected void removeSpi( final String key ) {
87
    root.remove( key );
88
89
    try {
90
      flush();
91
    } catch( final BackingStoreException ex ) {
92
      problem( ex );
93
    }
94
  }
95
96
  @Override
97
  protected void removeNodeSpi() throws BackingStoreException {
98
    isRemoved = true;
99
    flush();
100
  }
101
102
  @Override
103
  protected String[] keysSpi() throws BackingStoreException {
104
    return root.keySet().toArray( new String[ root.keySet().size() ] );
105
  }
106
107
  @Override
108
  protected String[] childrenNamesSpi() throws BackingStoreException {
109
    return children.keySet().toArray( new String[ children.keySet().size() ] );
110
  }
111
112
  @Override
113
  protected FilePreferences childSpi( final String name ) {
114
    FilePreferences child = children.get( name );
115
116
    if( child == null || child.isRemoved() ) {
117
      child = new FilePreferences( this, name );
118
      children.put( name, child );
119
    }
120
121
    return child;
122
  }
123
124
  @Override
125
  protected void syncSpi() throws BackingStoreException {
126
    if( isRemoved() ) {
127
      return;
128
    }
129
130
    final File file = FilePreferencesFactory.getPreferencesFile();
131
132
    if( !file.exists() ) {
133
      return;
134
    }
135
136
    synchronized( file ) {
137
      final Properties p = new Properties();
138
139
      try {
140
        p.load( new FileInputStream( file ) );
141
142
        final String path = getPath();
143
        final Enumeration<?> pnen = p.propertyNames();
144
145
        while( pnen.hasMoreElements() ) {
146
          final String propKey = (String)pnen.nextElement();
147
148
          if( propKey.startsWith( path ) ) {
149
            final String subKey = propKey.substring( path.length() );
150
151
            // Only load immediate descendants
152
            if( subKey.indexOf( '.' ) == -1 ) {
153
              root.put( subKey, p.getProperty( propKey ) );
154
            }
155
          }
156
        }
157
      } catch( final IOException e ) {
158
        throw new BackingStoreException( e );
159
      }
160
    }
161
  }
162
163
  private String getPath() {
164
    final FilePreferences parent = (FilePreferences)parent();
165
166
    return parent == null ? "" : parent.getPath() + name() + '.';
167
  }
168
169
  @Override
170
  protected void flushSpi() throws BackingStoreException {
171
    final File file = FilePreferencesFactory.getPreferencesFile();
172
173
    synchronized( file ) {
174
      final Properties p = new Properties();
175
176
      try {
177
        final String path = getPath();
178
179
        if( file.exists() ) {
180
          p.load( new FileInputStream( file ) );
181
182
          final List<String> toRemove = new ArrayList<>();
183
184
          // Make a list of all direct children of this node to be removed
185
          final Enumeration<?> pnen = p.propertyNames();
186
187
          while( pnen.hasMoreElements() ) {
188
            String propKey = (String)pnen.nextElement();
189
            if( propKey.startsWith( path ) ) {
190
              final String subKey = propKey.substring( path.length() );
191
192
              // Only do immediate descendants
193
              if( subKey.indexOf( '.' ) == -1 ) {
194
                toRemove.add( propKey );
195
              }
196
            }
197
          }
198
199
          // Remove them now that the enumeration is done with
200
          for( final String propKey : toRemove ) {
201
            p.remove( propKey );
202
          }
203
        }
204
205
        // If this node hasn't been removed, add back in any values
206
        if( !isRemoved ) {
207
          for( final String s : root.keySet() ) {
208
            p.setProperty( path + s, root.get( s ) );
209
          }
210
        }
211
212
        p.store( new FileOutputStream( file ), "FilePreferences" );
213
      } catch( final IOException e ) {
214
        throw new BackingStoreException( e );
215
      }
216
    }
217
  }
218
219
  private void problem( final BackingStoreException ex ) {
220
    throw new RuntimeException( ex );
221
  }
222
}
1223
A src/main/java/com/scrivenvar/preferences/FilePreferencesFactory.java
1
/*
2
 * Copyright 2016 David Croft and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preferences;
29
30
import static com.scrivenvar.Constants.APP_TITLE;
31
import java.io.File;
32
import java.nio.file.FileSystems;
33
import java.util.prefs.Preferences;
34
import java.util.prefs.PreferencesFactory;
35
36
/**
37
 * PreferencesFactory implementation that stores the preferences in a
38
 * user-defined file. Usage:
39
 * <pre>
40
 * System.setProperty( "java.util.prefs.PreferencesFactory",
41
 * FilePreferencesFactory.class.getName() );
42
 * </pre>
43
 * <p>
44
 * The file defaults to <code>$user.home/.scrivenvar</code>, but can be changed
45
 * using <code>-Dapplication.name=preferences</code> when running the
46
 * application, or by calling <code>System.setProperty</code> with the
47
 * "application.name" property.
48
 * </p>
49
 */
50
public class FilePreferencesFactory implements PreferencesFactory {
51
52
  private static File preferencesFile;
53
  private Preferences rootPreferences;
54
55
  @Override
56
  public Preferences systemRoot() {
57
    return userRoot();
58
  }
59
60
  @Override
61
  public synchronized Preferences userRoot() {
62
    if( rootPreferences == null ) {
63
      rootPreferences = new FilePreferences( null, "" );
64
    }
65
66
    return rootPreferences;
67
  }
68
69
  public synchronized static File getPreferencesFile() {
70
    if( preferencesFile == null ) {
71
      String prefsFile = getPreferencesFilename();
72
73
      preferencesFile = new File( prefsFile ).getAbsoluteFile();
74
    }
75
76
    return preferencesFile;
77
  }
78
79
  public static String getPreferencesFilename() {
80
    final String filename = System.getProperty( "application.name", APP_TITLE );
81
    return System.getProperty( "user.home" ) + getSeparator() + "." + filename;
82
  }
83
84
  public static String getSeparator() {
85
    return FileSystems.getDefault().getSeparator();
86
  }
87
}
188
A src/main/java/com/scrivenvar/processors/MarkdownVariableProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
31
import java.util.Map;
32
33
/**
34
 * Processes variables in the document and inserts their values into the
35
 * post-processed text.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public class MarkdownVariableProcessor extends AbstractProcessor<String> {
40
41
  private Map<String, String> definitions;
42
43
  /**
44
   * Constructs a new Markdown processor that can create HTML documents.
45
   *
46
   * @param successor Usually the HTML Preview Processor.
47
   */
48
  private MarkdownVariableProcessor( final Processor<String> successor ) {
49
    super( successor );
50
  }
51
52
  public MarkdownVariableProcessor(
53
    final Processor<String> successor, final Map<String, String> map ) {
54
    this( successor );
55
    setDefinitions( map );
56
  }
57
58
  /**
59
   *
60
   * @param text The document text that includes variables that should be
61
   * replaced with values when rendered as HTML.
62
   *
63
   * @return The text with all variables replaced.
64
   */
65
  @Override
66
  public String processLink( final String text ) {
67
    return replace( text, getDefinitions() );
68
  }
69
70
  private Map<String, String> getDefinitions() {
71
    return this.definitions;
72
  }
73
74
  private void setDefinitions( final Map<String, String> definitions ) {
75
    this.definitions = definitions;
76
  }
77
}
178
M src/main/java/com/scrivenvar/processors/ProcessorFactory.java
3535
import java.nio.file.Path;
3636
import java.util.Map;
37
import javafx.beans.value.ObservableValue;
3738
3839
/**
...
105106
  private Processor<String> getTerminalProcessChain() {
106107
    if( this.terminalProcessChain == null ) {
107
      this.terminalProcessChain = createTerminalProcessChain();
108
      this.terminalProcessChain = createCommonChain();
108109
    }
109110
110111
    return this.terminalProcessChain;
111112
  }
112113
113
  private Processor<String> createTerminalProcessChain() {
114
  /**
115
   * Creates and links the processors at the end of the processing chain.
116
   *
117
   * @return A markdown, caret replacement, and preview pane processor chain.
118
   */
119
  private Processor<String> createCommonChain() {
114120
    final Processor<String> hpp = new HTMLPreviewProcessor( getPreviewPane() );
115121
    final Processor<String> mcrp = new CaretReplacementProcessor( hpp );
116122
    final Processor<String> mpp = new MarkdownProcessor( mcrp );
117123
118124
    return mpp;
125
  }
126
127
  private Processor<String> createInsertionProcessor(
128
    final Processor<String> tpc, final ObservableValue<Integer> caret ) {
129
    return new MarkdownCaretInsertionProcessor( tpc, caret );
119130
  }
120131
121132
  protected Processor<String> createMarkdownProcessor( final FileEditorTab tab ) {
122
    final Processor<String> bp = getTerminalProcessChain();
123
    final Processor<String> xcip = new MarkdownCaretInsertionProcessor( bp, tab.caretPositionProperty() );
124
    final Processor<String> vp = new VariableProcessor( xcip, getResolvedMap() );
133
    final ObservableValue<Integer> caret = tab.caretPositionProperty();
134
    final Processor<String> tpc = getTerminalProcessChain();
135
    final Processor<String> cip = createInsertionProcessor( tpc, caret );
136
    final Processor<String> vp = new MarkdownVariableProcessor( cip, getResolvedMap() );
125137
126138
    return vp;
127139
  }
128140
129141
  protected Processor<String> createRMarkdownProcessor( final FileEditorTab tab ) {
130
    return createMarkdownProcessor( tab );
142
    final ObservableValue<Integer> caret = tab.caretPositionProperty();
143
    final Processor<String> tpc = getTerminalProcessChain();
144
    final Processor<String> cip = createInsertionProcessor( tpc, caret );
145
    final Processor<String> rp = new RProcessor( cip );
146
    
147
    return rp;
131148
  }
132149
133150
  protected Processor<String> createXMLProcessor( final FileEditorTab tab ) {
134
    final Processor<String> bp = getTerminalProcessChain();
135
    final Processor<String> xmlp = new XMLProcessor( bp, tab.getPath() );
151
    final Processor<String> tpc = getTerminalProcessChain();
152
    final Processor<String> xmlp = new XMLProcessor( tpc, tab.getPath() );
136153
    final Processor<String> xcip = new XMLCaretInsertionProcessor( xmlp, tab.caretPositionProperty() );
137
    final Processor<String> vp = new VariableProcessor( xcip, getResolvedMap() );
154
    final Processor<String> vp = new MarkdownVariableProcessor( xcip, getResolvedMap() );
138155
139156
    return vp;
A src/main/java/com/scrivenvar/processors/RProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import javax.script.ScriptEngine;
31
import org.renjin.script.RenjinScriptEngineFactory;
32
33
/**
34
 * Transforms an R document into markdown using knitr:
35
 *
36
 * @author White Magic Software, Ltd.
37
 */
38
public class RProcessor extends AbstractProcessor<String> {
39
40
  public RProcessor( Processor<String> processor ) {
41
    super( processor );
42
  }
43
44
  @Override
45
  public String processLink( final String text ) {
46
    System.out.println( "Renjin + Knitr Smackdown" );
47
48
    RenjinScriptEngineFactory factory = new RenjinScriptEngineFactory();
49
    ScriptEngine engine = factory.getScriptEngine();
50
51
    return text;
52
  }
53
}
154
D src/main/java/com/scrivenvar/processors/VariableProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.processors.text.TextReplacementFactory;
31
import com.scrivenvar.processors.text.TextReplacer;
32
import java.util.Map;
33
34
/**
35
 * Processes variables in the document and inserts their values into the
36
 * post-processed text.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
public class VariableProcessor extends AbstractProcessor<String> {
41
42
  private Map<String, String> definitions;
43
44
  /**
45
   * Constructs a new Markdown processor that can create HTML documents.
46
   *
47
   * @param successor Usually the HTML Preview Processor.
48
   */
49
  private VariableProcessor( final Processor<String> successor ) {
50
    super( successor );
51
  }
52
53
  public VariableProcessor(
54
    final Processor<String> successor,
55
    final Map<String, String> map ) {
56
    this( successor );
57
    setDefinitions( map );
58
  }
59
60
  /**
61
   *
62
   * @param text The document text that includes variables that should be
63
   * replaced with values when rendered as HTML.
64
   *
65
   * @return The text with all variables replaced.
66
   */
67
  @Override
68
  public String processLink( final String text ) {
69
    final TextReplacer tr = TextReplacementFactory.getTextReplacer( text.length() );
70
71
    return tr.replace( text, getDefinitions() );
72
  }
73
74
  private Map<String, String> getDefinitions() {
75
    return this.definitions;
76
  }
77
78
  private void setDefinitions( final Map<String, String> definitions ) {
79
    this.definitions = definitions;
80
  }
81
}
821
M src/main/java/com/scrivenvar/processors/text/AhoCorasickReplacer.java
6161
    int index = 0;
6262
63
    // Replace all instances with dereferenced variables.
6364
    for( final Emit emit : builder.build().parseText( text ) ) {
6465
      sb.append( text.substring( index, emit.getStart() ) );
M src/main/java/com/scrivenvar/processors/text/TextReplacementFactory.java
2828
package com.scrivenvar.processors.text;
2929
30
import java.util.Map;
31
3032
/**
3133
 * Used to generate a class capable of efficiently replacing variable
3234
 * definitions with their values.
3335
 *
3436
 * @author White Magic Software, Ltd.
3537
 */
36
public class TextReplacementFactory {
38
public final class TextReplacementFactory {
39
40
  private final static TextReplacer APACHE = new StringUtilsReplacer();
41
  private final static TextReplacer AHO_CORASICK = new AhoCorasickReplacer();
3742
3843
  /**
...
4954
    //
5055
    // Ssee http://stackoverflow.com/a/40836618/59087
51
    return length < 1500
52
      ? new StringUtilsReplacer()
53
      : new AhoCorasickReplacer();
56
    return length < 1500 ? APACHE : AHO_CORASICK;
57
  }
58
59
  /**
60
   * Convenience method to instantiate a suitable text replacer algorithm and
61
   * perform a replacement using the given map.
62
   *
63
   * @param text The text containing zero or more variables to replace.
64
   * @param map The map of variables to their dereferenced values.
65
   *
66
   * @return The text with all variables replaced.
67
   */
68
  public static String replace( final String text, final Map<String, String> map ) {
69
    return getTextReplacer( text.length() ).replace( text, map );
5470
  }
5571
}
D src/main/java/com/scrivenvar/service/events/AlertMessage.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events;
29
30
/**
31
 *
32
 * @author White Magic Software, Ltd.
33
 */
34
public interface AlertMessage {
35
36
  /**
37
   * Dialog box title.
38
   *
39
   * @return A non-null string to use as the title for the dialog.
40
   */
41
  public String getTitle();
42
43
  /**
44
   * Dialog box message content.
45
   *
46
   * @return A non-null string to use as the alert message for the dialog.
47
   */
48
  public String getContent();
49
}
501
D src/main/java/com/scrivenvar/service/events/AlertService.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events;
29
30
import javafx.scene.control.Alert;
31
import javafx.scene.control.ButtonType;
32
import javafx.stage.Window;
33
34
/**
35
 * Provides the application with a uniform way to create alert dialogs.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public interface AlertService {
40
  public static final ButtonType YES = ButtonType.YES;
41
  public static final ButtonType NO = ButtonType.NO;
42
  public static final ButtonType CANCEL = ButtonType.CANCEL;
43
44
  /**
45
   * Called to set the window used as the parent for the alert dialogs.
46
   *
47
   * @param window
48
   */
49
  public void setWindow( Window window );
50
51
  /**
52
   * Constructs a default alert message text for a modal alert dialog.
53
   *
54
   * @param title The dialog box message title.
55
   * @param message The dialog box message content (needs formatting).
56
   * @param args The arguments to the message content that must be formatted.
57
   *
58
   * @return The message suitable for building a modal alert dialog.
59
   */
60
  public AlertMessage createAlertMessage(
61
    String title,
62
    String message,
63
    Object... args );
64
65
  /**
66
   * Creates an alert of alert type error with a message showing the cause of
67
   * the error.
68
   *
69
   * @param alertMessage The error message, title, and possibly more details.
70
   *
71
   * @return A modal alert dialog box ready to display using showAndWait.
72
   */
73
  public Alert createAlertError( AlertMessage alertMessage );
74
75
  /**
76
   * Creates an alert of alert type confirmation with Yes/No/Cancel buttons.
77
   *
78
   * @param alertMessage The message, title, and possibly more details.
79
   *
80
   * @return A modal alert dialog box ready to display using showAndWait.
81
   */
82
  public Alert createAlertConfirmation( AlertMessage alertMessage );
83
}
841
A src/main/java/com/scrivenvar/service/events/Notification.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events;
29
30
/**
31
 * Represents a message that contains a title and content.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public interface Notification {
36
37
  /**
38
   * Alert title.
39
   *
40
   * @return A non-null string to use as alert message title.
41
   */
42
  public String getTitle();
43
44
  /**
45
   * Alert message content.
46
   *
47
   * @return A non-null string that contains information for the user.
48
   */
49
  public String getContent();
50
}
151
A src/main/java/com/scrivenvar/service/events/NotifyService.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events;
29
30
import javafx.scene.control.Alert;
31
import javafx.scene.control.ButtonType;
32
import javafx.stage.Window;
33
34
/**
35
 * Provides the application with a uniform way to notify the user of events.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public interface NotifyService {
40
  public static final ButtonType YES = ButtonType.YES;
41
  public static final ButtonType NO = ButtonType.NO;
42
  public static final ButtonType CANCEL = ButtonType.CANCEL;
43
44
  /**
45
   * Called to set the window used as the parent for the alert dialogs.
46
   *
47
   * @param window
48
   */
49
  public void setWindow( Window window );
50
51
  /**
52
   * Constructs a default alert message text for a modal alert dialog.
53
   *
54
   * @param title The dialog box message title.
55
   * @param message The dialog box message content (needs formatting).
56
   * @param args The arguments to the message content that must be formatted.
57
   *
58
   * @return The message suitable for building a modal alert dialog.
59
   */
60
  public Notification createNotification(
61
    String title,
62
    String message,
63
    Object... args );
64
65
  /**
66
   * Creates an alert of alert type error with a message showing the cause of
67
   * the error.
68
   *
69
   * @param message The error message, title, and possibly more details.
70
   *
71
   * @return A modal alert dialog box ready to display using showAndWait.
72
   */
73
  public Alert createError( Notification message );
74
75
  /**
76
   * Creates an alert of alert type confirmation with Yes/No/Cancel buttons.
77
   *
78
   * @param message The message, title, and possibly more details.
79
   *
80
   * @return A modal alert dialog box ready to display using showAndWait.
81
   */
82
  public Alert createConfirmation( Notification message );
83
}
184
D src/main/java/com/scrivenvar/service/events/impl/DefaultAlertMessage.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import com.scrivenvar.service.events.AlertMessage;
31
import java.text.MessageFormat;
32
33
/**
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public class DefaultAlertMessage implements AlertMessage {
38
39
  private final String title;
40
  private final String content;
41
42
  /**
43
   * Constructs a default alert message text for an alert modal dialog.
44
   * 
45
   * @param title The dialog box message title.
46
   * @param message The dialog box message content (needs formatting).
47
   * @param args The arguments to the message content that must be formatted.
48
   */
49
  public DefaultAlertMessage(
50
    final String title,
51
    final String message,
52
    final Object... args ) {
53
    this.title = title;
54
    this.content = MessageFormat.format( message, args );
55
  }
56
57
  @Override
58
  public String getTitle() {
59
    return this.title;
60
  }
61
62
  @Override
63
  public String getContent() {
64
    return this.content;
65
  }
66
}
671
D src/main/java/com/scrivenvar/service/events/impl/DefaultAlertService.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import com.scrivenvar.service.events.AlertMessage;
31
import com.scrivenvar.service.events.AlertService;
32
import javafx.scene.control.Alert;
33
import javafx.scene.control.Alert.AlertType;
34
import static javafx.scene.control.Alert.AlertType.CONFIRMATION;
35
import static javafx.scene.control.Alert.AlertType.ERROR;
36
import javafx.stage.Window;
37
38
/**
39
 * Provides the ability to create error alert boxes.
40
 *
41
 * @author White Magic Software, Ltd.
42
 */
43
public final class DefaultAlertService implements AlertService {
44
45
  private Window window;
46
47
  public DefaultAlertService() {
48
  }
49
50
  public DefaultAlertService( final Window window ) {
51
    this.window = window;
52
  }
53
54
  @Override
55
  public AlertMessage createAlertMessage(
56
    final String title,
57
    final String message,
58
    final Object... args ) {
59
    return new DefaultAlertMessage( title, message, args );
60
  }
61
62
  private Alert createAlertDialog(
63
    final AlertType alertType,
64
    final AlertMessage message ) {
65
66
    final Alert alert = new Alert( alertType );
67
68
    alert.setDialogPane( new ButtonOrderPane() );
69
    alert.setTitle( message.getTitle() );
70
    alert.setHeaderText( null );
71
    alert.setContentText( message.getContent() );
72
    alert.initOwner( getWindow() );
73
74
    return alert;
75
  }
76
77
  @Override
78
  public Alert createAlertConfirmation( final AlertMessage message ) {
79
    final Alert alert = createAlertDialog( CONFIRMATION, message );
80
81
    alert.getButtonTypes().setAll( YES, NO, CANCEL );
82
83
    return alert;
84
  }
85
86
  @Override
87
  public Alert createAlertError( final AlertMessage message ) {
88
    return createAlertDialog( ERROR, message );
89
  }
90
91
  private Window getWindow() {
92
    return this.window;
93
  }
94
95
  @Override
96
  public void setWindow( Window window ) {
97
    this.window = window;
98
  }
99
}
1001
A src/main/java/com/scrivenvar/service/events/impl/DefaultNotification.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import com.scrivenvar.service.events.Notification;
31
import java.text.MessageFormat;
32
33
/**
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public class DefaultNotification implements Notification {
38
39
  private final String title;
40
  private final String content;
41
42
  /**
43
   * Constructs default message text for a notification.
44
   * 
45
   * @param title The message title.
46
   * @param message The message content (needs formatting).
47
   * @param args The arguments to the message content that must be formatted.
48
   */
49
  public DefaultNotification(
50
    final String title,
51
    final String message,
52
    final Object... args ) {
53
    this.title = title;
54
    this.content = MessageFormat.format( message, args );
55
  }
56
57
  @Override
58
  public String getTitle() {
59
    return this.title;
60
  }
61
62
  @Override
63
  public String getContent() {
64
    return this.content;
65
  }
66
}
167
A src/main/java/com/scrivenvar/service/events/impl/DefaultNotifyService.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import com.scrivenvar.service.events.NotifyService;
31
import javafx.scene.control.Alert;
32
import javafx.scene.control.Alert.AlertType;
33
import static javafx.scene.control.Alert.AlertType.CONFIRMATION;
34
import static javafx.scene.control.Alert.AlertType.ERROR;
35
import javafx.stage.Window;
36
import com.scrivenvar.service.events.Notification;
37
38
/**
39
 * Provides the ability to notify the user of problems.
40
 *
41
 * @author White Magic Software, Ltd.
42
 */
43
public final class DefaultNotifyService implements NotifyService {
44
45
  private Window window;
46
47
  public DefaultNotifyService() {
48
  }
49
50
  public DefaultNotifyService( final Window window ) {
51
    this.window = window;
52
  }
53
54
  /**
55
   * Contains all the information that the user needs to know about a problem.
56
   * 
57
   * @param title The context for the message.
58
   * @param message The message content (formatted with the given args).
59
   * @param args Parameters for the message content.
60
   * @return 
61
   */
62
  @Override
63
  public Notification createNotification(
64
    final String title,
65
    final String message,
66
    final Object... args ) {
67
    return new DefaultNotification( title, message, args );
68
  }
69
70
  private Alert createAlertDialog(
71
    final AlertType alertType,
72
    final Notification message ) {
73
74
    final Alert alert = new Alert( alertType );
75
76
    alert.setDialogPane( new ButtonOrderPane() );
77
    alert.setTitle( message.getTitle() );
78
    alert.setHeaderText( null );
79
    alert.setContentText( message.getContent() );
80
    alert.initOwner( getWindow() );
81
82
    return alert;
83
  }
84
85
  @Override
86
  public Alert createConfirmation( final Notification message ) {
87
    final Alert alert = createAlertDialog( CONFIRMATION, message );
88
89
    alert.getButtonTypes().setAll( YES, NO, CANCEL );
90
91
    return alert;
92
  }
93
94
  @Override
95
  public Alert createError( final Notification message ) {
96
    return createAlertDialog( ERROR, message );
97
  }
98
99
  private Window getWindow() {
100
    return this.window;
101
  }
102
103
  @Override
104
  public void setWindow( Window window ) {
105
    this.window = window;
106
  }
107
}
1108
D src/main/resources/META-INF/services/com.scrivenvar.service.events.AlertService
1
com.scrivenvar.service.events.impl.DefaultAlertService
1
A src/main/resources/META-INF/services/com.scrivenvar.service.events.NotifyService
1
1
com.scrivenvar.service.events.impl.DefaultNotifyService