Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M .gitignore
66
/private
77
.nb-gradle-properties
8
src/main/resources/com/scrivenvar/build.sh
8
scrivenvar.pro
99
M CREDITS.md
99
  * Vladimir Schneider: [flexmark](https://website.com)
1010
  * Jens Deters: [FontAwesomeFX](https://bitbucket.org/Jerady/fontawesomefx)
11
  * Apache Tika Team: [Apache Tika](https://tika.apache.org/)
1112
M README.md
1212
1313
* User-defined variables
14
* Preview variable values in real time
14
* Recursive variable definitions
15
* Real-time document preview with variable substitution
1516
* Platform independent (Windows, Linux, MacOS)
16
* Auto-insert variable names by typing in values followed by `Control+Space`
17
* Auto-insert variable names pressing `Control+Space`
1718
1819
Future Features
1920
---
2021
* Spell check
22
* Search and replace, with or without variables
2123
* XML and XSL processing
22
* Search and replace text with variables
2324
* R integration using [Rserve](https://rforge.net/Rserve/)
25
* Re-organize variable names
2426
2527
Requirements
M build.gradle
2121
	compile group: 'com.miglayout', name: 'miglayout-javafx', version: '5.0'
2222
	compile group: 'de.jensd', name: 'fontawesomefx-fontawesome', version: '4.5.0'
23
  compile group: 'commons-configuration', name: 'commons-configuration', version: '1.10'
23
  compile group: 'org.ahocorasick', name: 'ahocorasick', version: '0.3.0'
2424
  compile group: 'com.vladsch.flexmark', name: 'flexmark', version: '0.6.1'
2525
  compile group: 'com.vladsch.flexmark', name: 'flexmark-ext-gfm-tables', version: '0.6.1'
26
  compile group: 'org.yaml', name: 'snakeyaml', version: '1.17'
2726
  compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.8.4'
2827
  compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.8.4'
2928
  compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.8.4'
3029
  compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.8.4'
31
  compile group: 'org.ahocorasick', name: 'ahocorasick', version: '0.3.0'
32
  compile group: 'junit', name: 'junit', version: '4.4'
30
  compile group: 'org.yaml', name: 'snakeyaml', version: '1.17'
31
  compile group: 'com.googlecode.juniversalchardet', name: 'juniversalchardet', version: '1.0.3'
32
  compile group: 'commons-configuration', name: 'commons-configuration', version: '1.10'
3333
}
3434
3535
jar {
3636
	baseName = 'scrivenvar'
37
  
38
  from {
39
    (configurations.runtime).collect {
40
      it.isDirectory() ? it : zipTree(it)
41
    }
42
  }
43
    
3744
	manifest {
3845
		attributes 'Main-Class': mainClassName,
M images/screenshot.png
Binary file
M src/main/java/com/scrivenvar/FileEditorTab.java
3232
import com.scrivenvar.service.events.AlertMessage;
3333
import com.scrivenvar.service.events.AlertService;
34
import java.io.IOException;
34
import java.nio.charset.Charset;
3535
import java.nio.file.Files;
3636
import java.nio.file.Path;
37
import static java.util.Locale.ENGLISH;
3738
import java.util.function.Consumer;
3839
import javafx.application.Platform;
3940
import javafx.beans.binding.Bindings;
4041
import javafx.beans.property.BooleanProperty;
4142
import javafx.beans.property.ReadOnlyBooleanProperty;
4243
import javafx.beans.property.ReadOnlyBooleanWrapper;
4344
import javafx.beans.property.SimpleBooleanProperty;
4445
import javafx.event.Event;
45
import javafx.scene.control.Alert;
4646
import javafx.scene.control.SplitPane;
4747
import javafx.scene.control.Tab;
...
5454
import org.fxmisc.wellbehaved.event.EventPattern;
5555
import org.fxmisc.wellbehaved.event.InputMap;
56
import org.mozilla.universalchardet.UniversalDetector;
5657
5758
/**
...
6768
  private EditorPane editorPane;
6869
  private HTMLPreviewPane previewPane;
70
71
  /**
72
   * Character encoding used by the file (or default encoding if none found).
73
   */
74
  private Charset encoding;
6975
7076
  private final ReadOnlyBooleanWrapper modified = new ReadOnlyBooleanWrapper();
...
8894
8995
  private void updateTab() {
90
    final Path filePath = getPath();
91
92
    setText( getFilename( filePath ) );
96
    setText( getTabTitle() );
9397
    setGraphic( getModifiedMark() );
94
    setTooltip( getTooltip( filePath ) );
98
    setTooltip( getTabTooltip() );
9599
  }
96100
97
  private String getFilename( final Path filePath ) {
101
  /**
102
   * Returns the base filename (without the directory names).
103
   *
104
   * @return The untitled text if the path hasn't been set.
105
   */
106
  private String getTabTitle() {
107
    final Path filePath = getPath();
108
98109
    return (filePath == null)
99110
      ? Messages.get( "FileEditor.untitled" )
100111
      : filePath.getFileName().toString();
101112
  }
102113
103
  private Tooltip getTooltip( final Path filePath ) {
114
  /**
115
   * Returns the full filename represented by the path.
116
   *
117
   * @return The untitled text if the path hasn't been set.
118
   */
119
  private Tooltip getTabTooltip() {
120
    final Path filePath = getPath();
121
104122
    return (filePath == null)
105123
      ? null
106124
      : new Tooltip( filePath.toString() );
107125
  }
108126
127
  /**
128
   * Returns a marker to indicate whether the file has been modified.
129
   *
130
   * @return "*" when the file has changed; otherwise null.
131
   */
109132
  private Text getModifiedMark() {
110133
    return isModified() ? new Text( "*" ) : null;
111134
  }
112135
113136
  /**
114137
   * Called when the user switches tab.
115138
   */
116139
  private void activated() {
140
    // Tab is closed or no longer active.
117141
    if( getTabPane() == null || !isSelected() ) {
118
      // Tab is closed or no longer active.
119142
      return;
120143
    }
121144
145
    // Switch to the tab without loading if the contents are already in memory.
122146
    if( getContent() != null ) {
123147
      getEditorPane().requestFocus();
124148
      return;
125149
    }
126150
127151
    // Load the text and update the preview before the undo manager.
128152
    load();
129153
130
    // Track undo requests (must not be called before load).
154
    // Track undo requests (*must* be called after load).
131155
    initUndoManager();
132156
    initSplitPane();
157
    initFocus();
133158
  }
134159
135160
  public void initSplitPane() {
136161
    final EditorPane editor = getEditorPane();
137162
    final HTMLPreviewPane preview = getPreviewPane();
138
139163
    final VirtualizedScrollPane<StyleClassedTextArea> editorScrollPane = editor.getScrollPane();
140164
141165
    // Make the preview pane scroll correspond to the editor pane scroll.
142166
    // Separate the edit and preview panels.
143
    final SplitPane splitPane = new SplitPane(
144
      editorScrollPane,
145
      preview.getWebView() );
146
    setContent( splitPane );
167
    setContent( new SplitPane( editorScrollPane, preview.getNode() ) );
168
  }
147169
148
    // Let the user edit.
149
    editor.requestFocus();
170
  private void initFocus() {
171
    getEditorPane().requestFocus();
150172
  }
151173
...
171193
  }
172194
173
  void load() {
195
  /**
196
   * Returns true if the given path exactly matches this tab's path.
197
   *
198
   * @param check The path to compare against.
199
   *
200
   * @return true The paths are the same.
201
   */
202
  public boolean isPath( final Path check ) {
174203
    final Path filePath = getPath();
175204
176
    if( filePath != null ) {
177
      try {
178
        final byte[] bytes = Files.readAllBytes( filePath );
179
        String markdown;
205
    return filePath == null ? false : filePath.equals( check );
206
  }
180207
181
        try {
182
          markdown = new String( bytes, getOptions().getEncoding() );
183
        } catch( Exception e ) {
184
          // Unsupported encodings and null pointers fallback here.
185
          markdown = new String( bytes );
186
        }
208
  /**
209
   * Reads the entire file contents from the path associated with this tab.
210
   */
211
  private void load() {
212
    final Path filePath = getPath();
187213
188
        getEditorPane().setText( markdown );
189
      } catch( IOException ex ) {
190
        final AlertMessage message = getAlertService().createAlertMessage(
191
          Messages.get( "FileEditor.loadFailed.title" ),
192
          Messages.get( "FileEditor.loadFailed.message" ),
193
          filePath,
194
          ex.getMessage()
214
    if( filePath != null ) {
215
      try {
216
        getEditorPane().setText( asString( Files.readAllBytes( filePath ) ) );
217
      } catch( Exception ex ) {
218
        alert(
219
          "FileEditor.loadFailed.title", "FileEditor.loadFailed.message", ex
195220
        );
196
197
        final Alert alert = getAlertService().createAlertError( message );
198
199
        alert.showAndWait();
200221
      }
201222
    }
202223
  }
203
204
  boolean save() {
205
    final String text = getEditorPane().getText();
206
207
    byte[] bytes;
208
209
    try {
210
      bytes = text.getBytes( getOptions().getEncoding() );
211
    } catch( Exception ex ) {
212
      bytes = text.getBytes();
213
    }
214224
225
  /**
226
   * Saves the entire file contents from the path associated with this tab.
227
   *
228
   * @return true The file has been saved.
229
   */
230
  public boolean save() {
215231
    try {
216
      Files.write( getPath(), bytes );
232
      Files.write( getPath(), asBytes( getEditorPane().getText() ) );
217233
      getEditorPane().getUndoManager().mark();
218234
      return true;
219
    } catch( IOException ex ) {
220
      final AlertService service = getAlertService();
221
      final AlertMessage message = service.createAlertMessage(
222
        Messages.get( "FileEditor.saveFailed.title" ),
223
        Messages.get( "FileEditor.saveFailed.message" ),
224
        getPath(),
225
        ex.getMessage()
235
    } catch( Exception ex ) {
236
      return alert(
237
        "FileEditor.saveFailed.title", "FileEditor.saveFailed.message", ex
226238
      );
239
    }
240
  }
227241
228
      final Alert alert = service.createAlertError( message );
242
  /**
243
   * Creates an alert dialog and waits for it to close.
244
   *
245
   * @param titleKey Resource bundle key for the alert dialog title.
246
   * @param messageKey Resource bundle key for the alert dialog message.
247
   * @param e The unexpected happening.
248
   *
249
   * @return false
250
   */
251
  private boolean alert( String titleKey, String messageKey, Exception e ) {
252
    final AlertService service = getAlertService();
229253
230
      alert.showAndWait();
231
      return false;
232
    }
254
    final AlertMessage message = service.createAlertMessage(
255
      Messages.get( titleKey ),
256
      Messages.get( messageKey ),
257
      getPath(),
258
      e.getMessage()
259
    );
260
261
    service.createAlertError( message ).showAndWait();
262
    return false;
263
  }
264
265
  /**
266
   * Returns a best guess at the file encoding. If the encoding could not be
267
   * detected, this will return the default charset for the JVM.
268
   *
269
   * @param bytes The bytes to perform character encoding detection.
270
   *
271
   * @return The character encoding.
272
   */
273
  private Charset detectEncoding( final byte[] bytes ) {
274
    final UniversalDetector detector = new UniversalDetector( null );
275
    detector.handleData( bytes, 0, bytes.length );
276
    detector.dataEnd();
277
278
    final String charset = detector.getDetectedCharset();
279
    final Charset charEncoding = charset == null
280
      ? Charset.defaultCharset()
281
      : Charset.forName( charset.toUpperCase( ENGLISH ) );
282
283
    detector.reset();
284
285
    return charEncoding;
286
  }
287
288
  /**
289
   * Converts the given string to an array of bytes using the encoding that was
290
   * originally detected (if any) and associated with this file.
291
   *
292
   * @param text The text to convert into the original file encoding.
293
   *
294
   * @return A series of bytes ready for writing to a file.
295
   */
296
  private byte[] asBytes( final String text ) {
297
    return text.getBytes( getEncoding() );
298
  }
299
300
  /**
301
   * Converts the given bytes into a Java String. This will call setEncoding
302
   * with the encoding detected by the CharsetDetector.
303
   *
304
   * @param text The text of unknown character encoding.
305
   *
306
   * @return The text, in its auto-detected encoding, as a String.
307
   */
308
  private String asString( final byte[] text ) {
309
    setEncoding( detectEncoding( text ) );
310
    return new String( text, getEncoding() );
233311
  }
234312
...
241319
  }
242320
243
  boolean isModified() {
321
  public boolean isModified() {
244322
    return this.modified.get();
245323
  }
...
261339
  }
262340
341
  /**
342
   * Forwards the request to the editor pane.
343
   *
344
   * @param <T> The type of event listener to add.
345
   * @param <U> The type of consumer to add.
346
   * @param event The event that should trigger updates to the listener.
347
   * @param consumer The listener to receive update events.
348
   */
263349
  public <T extends Event, U extends T> void addEventListener(
264350
    final EventPattern<? super T, ? extends U> event,
265351
    final Consumer<? super U> consumer ) {
266352
    getEditorPane().addEventListener( event, consumer );
267353
  }
268354
355
  /**
356
   * Forwards to the editor pane's listeners for keyboard events.
357
   *
358
   * @param map The new input map to replace the existing keyboard listener.
359
   */
269360
  public void addEventListener( final InputMap<InputEvent> map ) {
270361
    getEditorPane().addEventListener( map );
271362
  }
272363
364
  /**
365
   * Forwards to the editor pane's listeners for keyboard events.
366
   *
367
   * @param map The existing input map to remove from the keyboard listeners.
368
   */
273369
  public void removeEventListener( final InputMap<InputEvent> map ) {
274370
    getEditorPane().removeEventListener( map );
275371
  }
276372
373
  /**
374
   * Returns the editor pane, or creates one if it doesn't yet exist.
375
   *
376
   * @return The editor pane, never null.
377
   */
277378
  protected EditorPane getEditorPane() {
278379
    if( this.editorPane == null ) {
...
297398
298399
    return this.previewPane;
400
  }
401
402
  private Charset getEncoding() {
403
    return this.encoding;
404
  }
405
406
  private void setEncoding( final Charset encoding ) {
407
    this.encoding = encoding;
299408
  }
300409
}
M src/main/java/com/scrivenvar/FileEditorTabPane.java
255255
256256
  private List<FileEditorTab> openEditors( final List<File> files, final int activeIndex ) {
257
    final List<FileEditorTab> editors = new ArrayList<>();
257
    final int fileTally = files.size();
258
    final List<FileEditorTab> editors = new ArrayList<>( fileTally );
258259
    final List<Tab> tabs = getTabs();
259
    
260
260261
    // Close single unmodified "Untitled" tab.
261262
    if( tabs.size() == 1 ) {
262263
      final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 ).getUserData());
263264
264265
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
265266
        closeEditor( fileEditor, false );
266267
      }
267268
    }
268269
269
    for( int i = 0; i < files.size(); i++ ) {
270
      Path path = files.get( i ).toPath();
270
    for( int i = 0; i < fileTally; i++ ) {
271
      final Path path = files.get( i ).toPath();
271272
272273
      // Check whether file is already opened.
...
321322
322323
  boolean saveAllEditors() {
323
    final FileEditorTab[] allEditors = getAllEditors();
324
325324
    boolean success = true;
326
    for( FileEditorTab fileEditor : allEditors ) {
325
326
    for( FileEditorTab fileEditor : getAllEditors() ) {
327327
      if( !saveEditor( fileEditor ) ) {
328328
        success = false;
...
364364
      Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
365365
      Event.fireEvent( tab, event );
366
366367
      if( event.isConsumed() ) {
367368
        return false;
368369
      }
369370
    }
370371
371372
    getTabs().remove( tab );
373
372374
    if( tab.getOnClosed() != null ) {
373375
      Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
374376
    }
375377
376378
    return true;
377379
  }
378380
379381
  boolean closeAllEditors() {
380
    FileEditorTab[] allEditors = getAllEditors();
381
    FileEditorTab activeEditor = activeFileEditor.get();
382
    final FileEditorTab[] allEditors = getAllEditors();
383
    final FileEditorTab activeEditor = getActiveFileEditor();
382384
383385
    // try to save active tab first because in case the user decides to cancel,
384386
    // then it stays active
385387
    if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
386388
      return false;
387389
    }
390
391
    // This should be called any time a tab changes.
392
    persistPreferences();
388393
389394
    // save modified tabs
390395
    for( int i = 0; i < allEditors.length; i++ ) {
391
      FileEditorTab fileEditor = allEditors[ i ];
396
      final FileEditorTab fileEditor = allEditors[ i ];
397
392398
      if( fileEditor == activeEditor ) {
393399
        continue;
...
410416
      }
411417
    }
412
413
    saveState( allEditors, activeEditor );
414418
415419
    return getTabs().isEmpty();
...
428432
  }
429433
430
  private FileEditorTab findEditor( Path path ) {
434
  /**
435
   * Returns the file editor tab that has the given path.
436
   *
437
   * @return null No file editor tab for the given path was found.
438
   */
439
  private FileEditorTab findEditor( final Path path ) {
431440
    for( final Tab tab : getTabs() ) {
432
      final FileEditorTab fileEditor = (FileEditorTab)tab.getUserData();
441
      final FileEditorTab fileEditor = (FileEditorTab)tab;
433442
434
      if( path.equals( fileEditor.getPath() ) ) {
443
      System.out.println( "path = " + path );
444
      System.out.println( "fileEditor = " + fileEditor.isPath( path ) );
445
446
      if( fileEditor.isPath( path ) ) {
435447
        return fileEditor;
436448
      }
437449
    }
438450
439451
    return null;
440
  }
441
442
  private Settings getSettings() {
443
    return this.settings;
444452
  }
445453
...
494502
  }
495503
496
  private void saveLastDirectory( File file ) {
504
  private void saveLastDirectory( final File file ) {
497505
    getState().put( "lastDirectory", file.getParent() );
498506
  }
499507
500
  public void restoreState() {
508
  public void restorePreferences() {
501509
    int activeIndex = 0;
502510
503
    final Preferences state = getState();
504
    final String[] fileNames = Utils.getPrefsStrings( state, "file" );
505
    final String activeFileName = state.get( "activeFile", null );
511
    final Preferences preferences = getState();
512
    final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
513
    final String activeFileName = preferences.get( "activeFile", null );
506514
507515
    final ArrayList<File> files = new ArrayList<>( fileNames.length );
...
527535
  }
528536
529
  private void saveState( final FileEditorTab[] allEditors, final FileEditorTab activeEditor ) {
530
    final List<String> fileNames = new ArrayList<>( allEditors.length );
537
  public void persistPreferences() {
538
    final ObservableList<Tab> allEditors = getTabs();
539
    final List<String> fileNames = new ArrayList<>( allEditors.size() );
531540
532
    for( final FileEditorTab fileEditor : allEditors ) {
541
    for( final Tab tab : allEditors ) {
542
      final FileEditorTab fileEditor = (FileEditorTab)tab;
543
533544
      if( fileEditor.getPath() != null ) {
534545
        fileNames.add( fileEditor.getPath().toString() );
535546
      }
536547
    }
537548
538
    final Preferences state = getState();
539
    Utils.putPrefsStrings( state, "file", fileNames.toArray( new String[ fileNames.size() ] ) );
549
    final Preferences preferences = getState();
550
    Utils.putPrefsStrings( preferences, "file", fileNames.toArray( new String[ fileNames.size() ] ) );
551
552
    final FileEditorTab activeEditor = getActiveFileEditor();
540553
541554
    if( activeEditor != null && activeEditor.getPath() != null ) {
542
      state.put( "activeFile", activeEditor.getPath().toString() );
555
      preferences.put( "activeFile", activeEditor.getPath().toString() );
543556
    } else {
544
      state.remove( "activeFile" );
557
      preferences.remove( "activeFile" );
545558
    }
546559
  }
547560
548
  private Window getWindow() {
549
    return getScene().getWindow();
561
  private Settings getSettings() {
562
    return this.settings;
550563
  }
551564
552565
  protected Options getOptions() {
553566
    return this.options;
567
  }
568
569
  private Window getWindow() {
570
    return getScene().getWindow();
554571
  }
555572
556573
  protected Preferences getState() {
557574
    return getOptions().getState();
558575
  }
559
560576
}
561577
M src/main/java/com/scrivenvar/MainWindow.java
3434
import com.scrivenvar.editor.MarkdownEditorPane;
3535
import com.scrivenvar.editor.VariableNameInjector;
36
import com.scrivenvar.options.OptionsDialog;
37
import com.scrivenvar.preview.HTMLPreviewPane;
38
import com.scrivenvar.processors.HTMLPreviewProcessor;
39
import com.scrivenvar.processors.MarkdownCaretInsertionProcessor;
40
import com.scrivenvar.processors.MarkdownCaretReplacementProcessor;
41
import com.scrivenvar.processors.MarkdownProcessor;
42
import com.scrivenvar.processors.Processor;
43
import com.scrivenvar.processors.TextChangeProcessor;
44
import com.scrivenvar.processors.VariableProcessor;
45
import com.scrivenvar.service.Options;
46
import com.scrivenvar.util.Action;
47
import com.scrivenvar.util.ActionUtils;
48
import static com.scrivenvar.util.StageState.K_PANE_SPLIT_DEFINITION;
49
import static com.scrivenvar.util.StageState.K_PANE_SPLIT_EDITOR;
50
import com.scrivenvar.yaml.YamlParser;
51
import com.scrivenvar.yaml.YamlTreeAdapter;
52
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD;
53
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE;
54
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT;
55
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT;
56
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT;
57
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT;
58
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER;
59
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC;
60
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK;
61
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL;
62
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL;
63
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT;
64
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT;
65
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT;
66
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH;
67
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO;
68
import java.io.IOException;
69
import java.io.InputStream;
70
import java.util.Map;
71
import java.util.function.Function;
72
import java.util.prefs.Preferences;
73
import javafx.beans.binding.Bindings;
74
import javafx.beans.binding.BooleanBinding;
75
import javafx.beans.property.BooleanProperty;
76
import javafx.beans.property.SimpleBooleanProperty;
77
import javafx.beans.value.ObservableBooleanValue;
78
import javafx.beans.value.ObservableValue;
79
import javafx.collections.ListChangeListener.Change;
80
import javafx.collections.ObservableList;
81
import javafx.event.Event;
82
import javafx.scene.Node;
83
import javafx.scene.Scene;
84
import javafx.scene.control.Alert;
85
import javafx.scene.control.Alert.AlertType;
86
import javafx.scene.control.Menu;
87
import javafx.scene.control.MenuBar;
88
import javafx.scene.control.SplitPane;
89
import javafx.scene.control.Tab;
90
import javafx.scene.control.ToolBar;
91
import javafx.scene.control.TreeView;
92
import javafx.scene.image.Image;
93
import javafx.scene.image.ImageView;
94
import static javafx.scene.input.KeyCode.ESCAPE;
95
import javafx.scene.input.KeyEvent;
96
import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
97
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
98
import javafx.scene.layout.BorderPane;
99
import javafx.scene.layout.VBox;
100
import javafx.stage.Window;
101
import javafx.stage.WindowEvent;
102
import org.fxmisc.richtext.StyleClassedTextArea;
103
104
/**
105
 * Main window containing a tab pane in the center for file editors.
106
 *
107
 * @author Karl Tauber and White Magic Software, Ltd.
108
 */
109
public class MainWindow {
110
111
  private final Options options = Services.load( Options.class );
112
113
  private Scene scene;
114
115
  private TreeView<String> treeView;
116
  private FileEditorTabPane fileEditorPane;
117
  private DefinitionPane definitionPane;
118
119
  private VariableNameInjector variableNameInjector;
120
121
  private YamlTreeAdapter yamlTreeAdapter;
122
  private YamlParser yamlParser;
123
124
  private MenuBar menuBar;
125
126
  public MainWindow() {
127
    initLayout();
128
    initVariableNameInjector();
129
  }
130
131
  private void initLayout() {
132
    final SplitPane splitPane = new SplitPane(
133
      getDefinitionPane().getNode(),
134
      getFileEditorPane().getNode() );
135
136
    splitPane.setDividerPositions(
137
      getFloat( K_PANE_SPLIT_DEFINITION, .05f ),
138
      getFloat( K_PANE_SPLIT_EDITOR, .95f ) );
139
140
    // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restore-splitpane.html
141
    final BorderPane borderPane = new BorderPane();
142
    borderPane.setPrefSize( 1024, 800 );
143
    borderPane.setTop( createMenuBar() );
144
    borderPane.setCenter( splitPane );
145
    
146
    final Scene appScene = new Scene( borderPane );
147
    setScene( appScene );
148
    appScene.getStylesheets().add( Constants.STYLESHEET_PREVIEW );
149
    appScene.windowProperty().addListener(
150
      (observable, oldWindow, newWindow) -> {
151
        newWindow.setOnCloseRequest( e -> {
152
          if( !getFileEditorPane().closeAllEditors() ) {
153
            e.consume();
154
          }
155
        } );
156
157
        // Workaround JavaFX bug: deselect menubar if window loses focus.
158
        newWindow.focusedProperty().addListener(
159
          (obs, oldFocused, newFocused) -> {
160
            if( !newFocused ) {
161
              // Send an ESC key event to the menubar
162
              this.menuBar.fireEvent(
163
                new KeyEvent(
164
                  KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE,
165
                  false, false, false, false ) );
166
            }
167
          } );
168
      } );
169
  }
170
171
  private void initVariableNameInjector() {
172
    setVariableNameInjector( new VariableNameInjector(
173
      getFileEditorPane(),
174
      getDefinitionPane() )
175
    );
176
  }
177
178
  private Window getWindow() {
179
    return getScene().getWindow();
180
  }
181
182
  public Scene getScene() {
183
    return this.scene;
184
  }
185
186
  private void setScene( Scene scene ) {
187
    this.scene = scene;
188
  }
189
190
  /**
191
   * Creates a boolean property that is bound to another boolean value of the
192
   * active editor.
193
   */
194
  private BooleanProperty createActiveBooleanProperty(
195
    final Function<FileEditorTab, ObservableBooleanValue> func ) {
196
197
    final BooleanProperty b = new SimpleBooleanProperty();
198
    final FileEditorTab tab = getActiveFileEditor();
199
200
    if( tab != null ) {
201
      b.bind( func.apply( tab ) );
202
    }
203
204
    getFileEditorPane().activeFileEditorProperty().addListener(
205
      (observable, oldFileEditor, newFileEditor) -> {
206
        b.unbind();
207
208
        if( newFileEditor != null ) {
209
          b.bind( func.apply( newFileEditor ) );
210
        } else {
211
          b.set( false );
212
        }
213
      } );
214
215
    return b;
216
  }
217
218
  //---- File actions -------------------------------------------------------
219
  private void fileNew() {
220
    getFileEditorPane().newEditor();
221
  }
222
223
  private void fileOpen() {
224
    getFileEditorPane().openFileDialog();
225
  }
226
227
  private void fileClose() {
228
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
229
  }
230
231
  private void fileCloseAll() {
232
    getFileEditorPane().closeAllEditors();
233
  }
234
235
  private void fileSave() {
236
    getFileEditorPane().saveEditor( getActiveFileEditor() );
237
  }
238
239
  private void fileSaveAll() {
240
    getFileEditorPane().saveAllEditors();
241
  }
242
243
  private void fileExit() {
244
    final Window window = getWindow();
245
    Event.fireEvent( window,
246
      new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) );
247
  }
248
249
  //---- Tools actions ------------------------------------------------------
250
  private void toolsOptions() {
251
    new OptionsDialog( getWindow() ).showAndWait();
252
  }
253
254
  //---- Help actions -------------------------------------------------------
255
  private void helpAbout() {
256
    Alert alert = new Alert( AlertType.INFORMATION );
257
    alert.setTitle( Messages.get( "Dialog.about.title" ) );
258
    alert.setHeaderText( Messages.get( "Dialog.about.header" ) );
259
    alert.setContentText( Messages.get( "Dialog.about.content" ) );
260
    alert.setGraphic( new ImageView( new Image( LOGO_32 ) ) );
261
    alert.initOwner( getWindow() );
262
263
    alert.showAndWait();
264
  }
265
266
  private FileEditorTabPane getFileEditorPane() {
267
    if( this.fileEditorPane == null ) {
268
      this.fileEditorPane = createFileEditorPane();
269
    }
270
271
    return this.fileEditorPane;
272
  }
273
274
  private FileEditorTabPane createFileEditorPane() {
275
    // Create an editor pane to hold file editor tabs.
276
    final FileEditorTabPane editorPane = new FileEditorTabPane();
277
278
    // Make sure the text processor kicks off when new files are opened.
279
    final ObservableList<Tab> tabs = editorPane.getTabs();
280
281
    tabs.addListener( (Change<? extends Tab> change) -> {
282
      while( change.next() ) {
283
        if( change.wasAdded() ) {
284
          // Multiple tabs can be added simultaneously.
285
          for( final Tab tab : change.getAddedSubList() ) {
286
            addListener( (FileEditorTab)tab );
287
          }
288
        }
289
      }
290
    } );
291
292
    // After the processors are in place, restore the previously closed
293
    // tabs. Adding them will trigger the change event, above.
294
    editorPane.restoreState();
295
296
    return editorPane;
297
  }
298
299
  private MarkdownEditorPane getActiveEditor() {
300
    return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane());
301
  }
302
303
  private FileEditorTab getActiveFileEditor() {
304
    return getFileEditorPane().getActiveFileEditor();
305
  }
306
307
  /**
308
   * Listens for changes to tabs and their text editors.
309
   *
310
   * @see https://github.com/DaveJarvis/scrivenvar/issues/17
311
   * @see https://github.com/DaveJarvis/scrivenvar/issues/18
312
   *
313
   * @param tab The file editor tab that contains a text editor.
314
   */
315
  private void addListener( FileEditorTab tab ) {
316
    final HTMLPreviewPane previewPane = tab.getPreviewPane();
317
    final EditorPane editorPanel = tab.getEditorPane();
318
    final StyleClassedTextArea editor = editorPanel.getEditor();
319
320
    // TODO: Use a factory based on the filename extension. The default
321
    // extension will be for a markdown file (e.g., on file new).
322
    final Processor<String> hpp = new HTMLPreviewProcessor( previewPane );
323
    final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp );
324
    final Processor<String> mp = new MarkdownProcessor( mcrp );
325
    final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, editor );
326
    final Processor<String> vnp = new VariableProcessor( mcip, getResolvedMap() );
327
    final TextChangeProcessor tp = new TextChangeProcessor( vnp );
328
329
    editorPanel.addChangeListener( tp );
330
    editorPanel.addCaretParagraphListener(
331
      (final ObservableValue<? extends Integer> observable,
332
        final Integer oldValue, final Integer newValue) -> {
333
        
334
        // Kick off the processing chain at the variable processor when the
335
        // cursor changes paragraphs. This might cause some slight duplication
336
        // when the Enter key is pressed.
337
        vnp.processChain( editor.getText() );
338
      } );
339
  }
340
341
  protected DefinitionPane createDefinitionPane() {
342
    return new DefinitionPane( getTreeView() );
343
  }
344
345
  private DefinitionPane getDefinitionPane() {
346
    if( this.definitionPane == null ) {
347
      this.definitionPane = createDefinitionPane();
348
    }
349
350
    return this.definitionPane;
351
  }
352
353
  public MenuBar getMenuBar() {
354
    return menuBar;
355
  }
356
357
  public void setMenuBar( MenuBar menuBar ) {
358
    this.menuBar = menuBar;
359
  }
360
361
  public VariableNameInjector getVariableNameInjector() {
362
    return this.variableNameInjector;
363
  }
364
365
  public void setVariableNameInjector( VariableNameInjector variableNameInjector ) {
366
    this.variableNameInjector = variableNameInjector;
367
  }
368
369
  private float getFloat( final String key, final float defaultValue ) {
370
    return getPreferences().getFloat( key, defaultValue );
371
  }
372
373
  private Preferences getPreferences() {
374
    return getOptions().getState();
375
  }
376
377
  private Options getOptions() {
378
    return this.options;
379
  }
380
381
  private synchronized TreeView<String> getTreeView() throws RuntimeException {
382
    if( this.treeView == null ) {
383
      try {
384
        this.treeView = createTreeView();
385
      } catch( IOException ex ) {
386
387
        // TODO: Pop an error message.
388
        throw new RuntimeException( ex );
389
      }
390
    }
391
392
    return this.treeView;
393
  }
394
395
  private InputStream asStream( final String resource ) {
396
    return getClass().getResourceAsStream( resource );
397
  }
398
399
  private TreeView<String> createTreeView() throws IOException {
400
    // TODO: Associate variable file with path to current file.
401
    return getYamlTreeAdapter().adapt(
402
      asStream( "/com/scrivenvar/variables.yaml" ),
403
      get( "Pane.defintion.node.root.title" )
404
    );
405
  }
406
407
  private Map<String, String> getResolvedMap() {
408
    return getYamlParser().createResolvedMap();
409
  }
410
411
  private YamlTreeAdapter getYamlTreeAdapter() {
412
    if( this.yamlTreeAdapter == null ) {
413
      setYamlTreeAdapter( new YamlTreeAdapter( getYamlParser() ) );
414
    }
415
416
    return this.yamlTreeAdapter;
417
  }
418
419
  private void setYamlTreeAdapter( final YamlTreeAdapter yamlTreeAdapter ) {
420
    this.yamlTreeAdapter = yamlTreeAdapter;
421
  }
422
423
  private YamlParser getYamlParser() {
424
    if( this.yamlParser == null ) {
425
      setYamlParser( new YamlParser() );
426
    }
427
428
    return this.yamlParser;
429
  }
430
431
  private void setYamlParser( final YamlParser yamlParser ) {
432
    this.yamlParser = yamlParser;
433
  }
434
435
  private Node createMenuBar() {
436
    final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
437
438
    // File actions
439
    Action fileNewAction = new Action( Messages.get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
440
    Action fileOpenAction = new Action( Messages.get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
441
    Action fileCloseAction = new Action( Messages.get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
442
    Action fileCloseAllAction = new Action( Messages.get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
443
    Action fileSaveAction = new Action( Messages.get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
444
      createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
445
    Action fileSaveAllAction = new Action( Messages.get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
446
      Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
447
    Action fileExitAction = new Action( Messages.get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
448
449
    // Edit actions
450
    Action editUndoAction = new Action( Messages.get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
451
      e -> getActiveEditor().undo(),
452
      createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
453
    Action editRedoAction = new Action( Messages.get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
454
      e -> getActiveEditor().redo(),
455
      createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
456
457
    // Insert actions
458
    Action insertBoldAction = new Action( Messages.get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
459
      e -> getActiveEditor().surroundSelection( "**", "**" ),
460
      activeFileEditorIsNull );
461
    Action insertItalicAction = new Action( Messages.get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
462
      e -> getActiveEditor().surroundSelection( "*", "*" ),
463
      activeFileEditorIsNull );
464
    Action insertStrikethroughAction = new Action( Messages.get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
465
      e -> getActiveEditor().surroundSelection( "~~", "~~" ),
466
      activeFileEditorIsNull );
467
    Action insertBlockquoteAction = new Action( Messages.get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
468
      e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
469
      activeFileEditorIsNull );
470
    Action insertCodeAction = new Action( Messages.get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
471
      e -> getActiveEditor().surroundSelection( "`", "`" ),
472
      activeFileEditorIsNull );
473
    Action insertFencedCodeBlockAction = new Action( Messages.get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
474
      e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", Messages.get( "Main.menu.insert.fenced_code_block.prompt" ) ),
475
      activeFileEditorIsNull );
476
477
    Action insertLinkAction = new Action( Messages.get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
478
      e -> getActiveEditor().insertLink(),
479
      activeFileEditorIsNull );
480
    Action insertImageAction = new Action( Messages.get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT,
481
      e -> getActiveEditor().insertImage(),
482
      activeFileEditorIsNull );
483
484
    final Action[] headers = new Action[ 6 ];
485
486
    // Insert header actions (H1 ... H6)
487
    for( int i = 1; i <= 6; i++ ) {
488
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
489
      final String markup = String.format( "\n\n%s ", hashes );
490
      final String text = Messages.get( "Main.menu.insert.header_" + i );
491
      final String accelerator = "Shortcut+" + i;
492
      final String prompt = Messages.get( "Main.menu.insert.header_" + i + ".prompt" );
493
494
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
495
        e -> getActiveEditor().surroundSelection( markup, "", prompt ),
496
        activeFileEditorIsNull );
497
    }
498
499
    Action insertUnorderedListAction = new Action( Messages.get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
500
      e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
501
      activeFileEditorIsNull );
502
    Action insertOrderedListAction = new Action( Messages.get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
503
      e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
504
      activeFileEditorIsNull );
505
    Action insertHorizontalRuleAction = new Action( Messages.get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
506
      e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
507
      activeFileEditorIsNull );
508
509
    // Tools actions
510
    Action toolsOptionsAction = new Action( Messages.get( "Main.menu.tools.options" ), "Shortcut+,", null, e -> toolsOptions() );
511
512
    // Help actions
513
    Action helpAboutAction = new Action( Messages.get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
514
515
    //---- MenuBar ----
516
    Menu fileMenu = ActionUtils.createMenu( Messages.get( "Main.menu.file" ),
517
      fileNewAction,
518
      fileOpenAction,
519
      null,
520
      fileCloseAction,
521
      fileCloseAllAction,
522
      null,
523
      fileSaveAction,
524
      fileSaveAllAction,
525
      null,
526
      fileExitAction );
527
528
    Menu editMenu = ActionUtils.createMenu( Messages.get( "Main.menu.edit" ),
529
      editUndoAction,
530
      editRedoAction );
531
532
    Menu insertMenu = ActionUtils.createMenu( Messages.get( "Main.menu.insert" ),
533
      insertBoldAction,
534
      insertItalicAction,
535
      insertStrikethroughAction,
536
      insertBlockquoteAction,
537
      insertCodeAction,
538
      insertFencedCodeBlockAction,
539
      null,
540
      insertLinkAction,
541
      insertImageAction,
542
      null,
543
      headers[ 0 ],
544
      headers[ 1 ],
545
      headers[ 2 ],
546
      headers[ 3 ],
547
      headers[ 4 ],
548
      headers[ 5 ],
549
      null,
550
      insertUnorderedListAction,
551
      insertOrderedListAction,
552
      insertHorizontalRuleAction );
553
554
    Menu toolsMenu = ActionUtils.createMenu( Messages.get( "Main.menu.tools" ),
555
      toolsOptionsAction );
556
557
    Menu helpMenu = ActionUtils.createMenu( Messages.get( "Main.menu.help" ),
558
      helpAboutAction );
559
560
    menuBar = new MenuBar( fileMenu, editMenu, insertMenu, toolsMenu, helpMenu );
36
import com.scrivenvar.preview.HTMLPreviewPane;
37
import com.scrivenvar.processors.HTMLPreviewProcessor;
38
import com.scrivenvar.processors.MarkdownCaretInsertionProcessor;
39
import com.scrivenvar.processors.MarkdownCaretReplacementProcessor;
40
import com.scrivenvar.processors.MarkdownProcessor;
41
import com.scrivenvar.processors.Processor;
42
import com.scrivenvar.processors.TextChangeProcessor;
43
import com.scrivenvar.processors.VariableProcessor;
44
import com.scrivenvar.service.Options;
45
import com.scrivenvar.util.Action;
46
import com.scrivenvar.util.ActionUtils;
47
import static com.scrivenvar.util.StageState.K_PANE_SPLIT_DEFINITION;
48
import static com.scrivenvar.util.StageState.K_PANE_SPLIT_EDITOR;
49
import com.scrivenvar.yaml.YamlParser;
50
import com.scrivenvar.yaml.YamlTreeAdapter;
51
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD;
52
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE;
53
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT;
54
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT;
55
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT;
56
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT;
57
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER;
58
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC;
59
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK;
60
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL;
61
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL;
62
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT;
63
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT;
64
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT;
65
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH;
66
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO;
67
import java.io.IOException;
68
import java.io.InputStream;
69
import java.util.Map;
70
import java.util.function.Function;
71
import java.util.prefs.Preferences;
72
import javafx.beans.binding.Bindings;
73
import javafx.beans.binding.BooleanBinding;
74
import javafx.beans.property.BooleanProperty;
75
import javafx.beans.property.SimpleBooleanProperty;
76
import javafx.beans.value.ObservableBooleanValue;
77
import javafx.beans.value.ObservableValue;
78
import javafx.collections.ListChangeListener.Change;
79
import javafx.collections.ObservableList;
80
import javafx.event.Event;
81
import javafx.scene.Node;
82
import javafx.scene.Scene;
83
import javafx.scene.control.Alert;
84
import javafx.scene.control.Alert.AlertType;
85
import javafx.scene.control.Menu;
86
import javafx.scene.control.MenuBar;
87
import javafx.scene.control.SplitPane;
88
import javafx.scene.control.Tab;
89
import javafx.scene.control.ToolBar;
90
import javafx.scene.control.TreeView;
91
import javafx.scene.image.Image;
92
import javafx.scene.image.ImageView;
93
import static javafx.scene.input.KeyCode.ESCAPE;
94
import javafx.scene.input.KeyEvent;
95
import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
96
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
97
import javafx.scene.layout.BorderPane;
98
import javafx.scene.layout.VBox;
99
import javafx.stage.Window;
100
import javafx.stage.WindowEvent;
101
import org.fxmisc.richtext.StyleClassedTextArea;
102
import static com.scrivenvar.Messages.get;
103
import static com.scrivenvar.Messages.get;
104
import static com.scrivenvar.Messages.get;
105
import static com.scrivenvar.Messages.get;
106
import static com.scrivenvar.Messages.get;
107
import static com.scrivenvar.Messages.get;
108
import static com.scrivenvar.Messages.get;
109
110
/**
111
 * Main window containing a tab pane in the center for file editors.
112
 *
113
 * @author Karl Tauber and White Magic Software, Ltd.
114
 */
115
public class MainWindow {
116
117
  private final Options options = Services.load( Options.class );
118
119
  private Scene scene;
120
121
  private TreeView<String> treeView;
122
  private FileEditorTabPane fileEditorPane;
123
  private DefinitionPane definitionPane;
124
125
  private VariableNameInjector variableNameInjector;
126
127
  private YamlTreeAdapter yamlTreeAdapter;
128
  private YamlParser yamlParser;
129
130
  private MenuBar menuBar;
131
132
  public MainWindow() {
133
    initLayout();
134
    initVariableNameInjector();
135
  }
136
137
  private void initLayout() {
138
    final SplitPane splitPane = new SplitPane(
139
      getDefinitionPane().getNode(),
140
      getFileEditorPane().getNode() );
141
142
    splitPane.setDividerPositions(
143
      getFloat( K_PANE_SPLIT_DEFINITION, .05f ),
144
      getFloat( K_PANE_SPLIT_EDITOR, .95f ) );
145
146
    // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
147
    final BorderPane borderPane = new BorderPane();
148
    borderPane.setPrefSize( 1024, 800 );
149
    borderPane.setTop( createMenuBar() );
150
    borderPane.setCenter( splitPane );
151
    
152
    final Scene appScene = new Scene( borderPane );
153
    setScene( appScene );
154
    appScene.getStylesheets().add( Constants.STYLESHEET_PREVIEW );
155
    appScene.windowProperty().addListener(
156
      (observable, oldWindow, newWindow) -> {
157
        newWindow.setOnCloseRequest( e -> {
158
          if( !getFileEditorPane().closeAllEditors() ) {
159
            e.consume();
160
          }
161
        } );
162
163
        // Workaround JavaFX bug: deselect menubar if window loses focus.
164
        newWindow.focusedProperty().addListener(
165
          (obs, oldFocused, newFocused) -> {
166
            if( !newFocused ) {
167
              // Send an ESC key event to the menubar
168
              this.menuBar.fireEvent(
169
                new KeyEvent(
170
                  KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE,
171
                  false, false, false, false ) );
172
            }
173
          } );
174
      } );
175
  }
176
177
  private void initVariableNameInjector() {
178
    setVariableNameInjector( new VariableNameInjector(
179
      getFileEditorPane(),
180
      getDefinitionPane() )
181
    );
182
  }
183
184
  private Window getWindow() {
185
    return getScene().getWindow();
186
  }
187
188
  public Scene getScene() {
189
    return this.scene;
190
  }
191
192
  private void setScene( Scene scene ) {
193
    this.scene = scene;
194
  }
195
196
  /**
197
   * Creates a boolean property that is bound to another boolean value of the
198
   * active editor.
199
   */
200
  private BooleanProperty createActiveBooleanProperty(
201
    final Function<FileEditorTab, ObservableBooleanValue> func ) {
202
203
    final BooleanProperty b = new SimpleBooleanProperty();
204
    final FileEditorTab tab = getActiveFileEditor();
205
206
    if( tab != null ) {
207
      b.bind( func.apply( tab ) );
208
    }
209
210
    getFileEditorPane().activeFileEditorProperty().addListener(
211
      (observable, oldFileEditor, newFileEditor) -> {
212
        b.unbind();
213
214
        if( newFileEditor != null ) {
215
          b.bind( func.apply( newFileEditor ) );
216
        } else {
217
          b.set( false );
218
        }
219
      } );
220
221
    return b;
222
  }
223
224
  //---- File actions -------------------------------------------------------
225
  private void fileNew() {
226
    getFileEditorPane().newEditor();
227
  }
228
229
  private void fileOpen() {
230
    getFileEditorPane().openFileDialog();
231
  }
232
233
  private void fileClose() {
234
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
235
  }
236
237
  private void fileCloseAll() {
238
    getFileEditorPane().closeAllEditors();
239
  }
240
241
  private void fileSave() {
242
    getFileEditorPane().saveEditor( getActiveFileEditor() );
243
  }
244
245
  private void fileSaveAll() {
246
    getFileEditorPane().saveAllEditors();
247
  }
248
249
  private void fileExit() {
250
    final Window window = getWindow();
251
    Event.fireEvent( window,
252
      new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) );
253
  }
254
255
  //---- Help actions -------------------------------------------------------
256
  private void helpAbout() {
257
    Alert alert = new Alert( AlertType.INFORMATION );
258
    alert.setTitle( Messages.get( "Dialog.about.title" ) );
259
    alert.setHeaderText( Messages.get( "Dialog.about.header" ) );
260
    alert.setContentText( Messages.get( "Dialog.about.content" ) );
261
    alert.setGraphic( new ImageView( new Image( LOGO_32 ) ) );
262
    alert.initOwner( getWindow() );
263
264
    alert.showAndWait();
265
  }
266
267
  private FileEditorTabPane getFileEditorPane() {
268
    if( this.fileEditorPane == null ) {
269
      this.fileEditorPane = createFileEditorPane();
270
    }
271
272
    return this.fileEditorPane;
273
  }
274
275
  private FileEditorTabPane createFileEditorPane() {
276
    // Create an editor pane to hold file editor tabs.
277
    final FileEditorTabPane editorPane = new FileEditorTabPane();
278
279
    // Make sure the text processor kicks off when new files are opened.
280
    final ObservableList<Tab> tabs = editorPane.getTabs();
281
282
    tabs.addListener( (Change<? extends Tab> change) -> {
283
      while( change.next() ) {
284
        if( change.wasAdded() ) {
285
          // Multiple tabs can be added simultaneously.
286
          for( final Tab tab : change.getAddedSubList() ) {
287
            addListener( (FileEditorTab)tab );
288
          }
289
        }
290
      }
291
    } );
292
293
    // After the processors are in place, restorePreferences the previously closed
294
    // tabs. Adding them will trigger the change event, above.
295
    editorPane.restorePreferences();
296
297
    return editorPane;
298
  }
299
300
  private MarkdownEditorPane getActiveEditor() {
301
    return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane());
302
  }
303
304
  private FileEditorTab getActiveFileEditor() {
305
    return getFileEditorPane().getActiveFileEditor();
306
  }
307
308
  /**
309
   * Listens for changes to tabs and their text editors.
310
   *
311
   * @see https://github.com/DaveJarvis/scrivenvar/issues/17
312
   * @see https://github.com/DaveJarvis/scrivenvar/issues/18
313
   *
314
   * @param tab The file editor tab that contains a text editor.
315
   */
316
  private void addListener( FileEditorTab tab ) {
317
    final HTMLPreviewPane previewPane = tab.getPreviewPane();
318
    final EditorPane editorPanel = tab.getEditorPane();
319
    final StyleClassedTextArea editor = editorPanel.getEditor();
320
321
    // TODO: Use a factory based on the filename extension. The default
322
    // extension will be for a markdown file (e.g., on file new).
323
    final Processor<String> hpp = new HTMLPreviewProcessor( previewPane );
324
    final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp );
325
    final Processor<String> mp = new MarkdownProcessor( mcrp );
326
    final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, editor );
327
    final Processor<String> vnp = new VariableProcessor( mcip, getResolvedMap() );
328
    final TextChangeProcessor tp = new TextChangeProcessor( vnp );
329
330
    editorPanel.addChangeListener( tp );
331
    editorPanel.addCaretParagraphListener(
332
      (final ObservableValue<? extends Integer> observable,
333
        final Integer oldValue, final Integer newValue) -> {
334
        
335
        // Kick off the processing chain at the variable processor when the
336
        // cursor changes paragraphs. This might cause some slight duplication
337
        // when the Enter key is pressed.
338
        vnp.processChain( editor.getText() );
339
      } );
340
  }
341
342
  protected DefinitionPane createDefinitionPane() {
343
    return new DefinitionPane( getTreeView() );
344
  }
345
346
  private DefinitionPane getDefinitionPane() {
347
    if( this.definitionPane == null ) {
348
      this.definitionPane = createDefinitionPane();
349
    }
350
351
    return this.definitionPane;
352
  }
353
354
  public MenuBar getMenuBar() {
355
    return menuBar;
356
  }
357
358
  public void setMenuBar( MenuBar menuBar ) {
359
    this.menuBar = menuBar;
360
  }
361
362
  public VariableNameInjector getVariableNameInjector() {
363
    return this.variableNameInjector;
364
  }
365
366
  public void setVariableNameInjector( VariableNameInjector variableNameInjector ) {
367
    this.variableNameInjector = variableNameInjector;
368
  }
369
370
  private float getFloat( final String key, final float defaultValue ) {
371
    return getPreferences().getFloat( key, defaultValue );
372
  }
373
374
  private Preferences getPreferences() {
375
    return getOptions().getState();
376
  }
377
378
  private Options getOptions() {
379
    return this.options;
380
  }
381
382
  private synchronized TreeView<String> getTreeView() throws RuntimeException {
383
    if( this.treeView == null ) {
384
      try {
385
        this.treeView = createTreeView();
386
      } catch( IOException ex ) {
387
388
        // TODO: Pop an error message.
389
        throw new RuntimeException( ex );
390
      }
391
    }
392
393
    return this.treeView;
394
  }
395
396
  private InputStream asStream( final String resource ) {
397
    return getClass().getResourceAsStream( resource );
398
  }
399
400
  private TreeView<String> createTreeView() throws IOException {
401
    // TODO: Associate variable file with path to current file.
402
    return getYamlTreeAdapter().adapt(
403
      asStream( "/com/scrivenvar/variables.yaml" ),
404
      get( "Pane.defintion.node.root.title" )
405
    );
406
  }
407
408
  private Map<String, String> getResolvedMap() {
409
    return getYamlParser().createResolvedMap();
410
  }
411
412
  private YamlTreeAdapter getYamlTreeAdapter() {
413
    if( this.yamlTreeAdapter == null ) {
414
      setYamlTreeAdapter( new YamlTreeAdapter( getYamlParser() ) );
415
    }
416
417
    return this.yamlTreeAdapter;
418
  }
419
420
  private void setYamlTreeAdapter( final YamlTreeAdapter yamlTreeAdapter ) {
421
    this.yamlTreeAdapter = yamlTreeAdapter;
422
  }
423
424
  private YamlParser getYamlParser() {
425
    if( this.yamlParser == null ) {
426
      setYamlParser( new YamlParser() );
427
    }
428
429
    return this.yamlParser;
430
  }
431
432
  private void setYamlParser( final YamlParser yamlParser ) {
433
    this.yamlParser = yamlParser;
434
  }
435
436
  private Node createMenuBar() {
437
    final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
438
439
    // File actions
440
    Action fileNewAction = new Action( Messages.get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
441
    Action fileOpenAction = new Action( Messages.get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
442
    Action fileCloseAction = new Action( Messages.get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
443
    Action fileCloseAllAction = new Action( Messages.get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
444
    Action fileSaveAction = new Action( Messages.get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
445
      createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
446
    Action fileSaveAllAction = new Action( Messages.get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
447
      Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
448
    Action fileExitAction = new Action( Messages.get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
449
450
    // Edit actions
451
    Action editUndoAction = new Action( Messages.get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
452
      e -> getActiveEditor().undo(),
453
      createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
454
    Action editRedoAction = new Action( Messages.get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
455
      e -> getActiveEditor().redo(),
456
      createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
457
458
    // Insert actions
459
    Action insertBoldAction = new Action( Messages.get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
460
      e -> getActiveEditor().surroundSelection( "**", "**" ),
461
      activeFileEditorIsNull );
462
    Action insertItalicAction = new Action( Messages.get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
463
      e -> getActiveEditor().surroundSelection( "*", "*" ),
464
      activeFileEditorIsNull );
465
    Action insertStrikethroughAction = new Action( Messages.get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
466
      e -> getActiveEditor().surroundSelection( "~~", "~~" ),
467
      activeFileEditorIsNull );
468
    Action insertBlockquoteAction = new Action( Messages.get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
469
      e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
470
      activeFileEditorIsNull );
471
    Action insertCodeAction = new Action( Messages.get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
472
      e -> getActiveEditor().surroundSelection( "`", "`" ),
473
      activeFileEditorIsNull );
474
    Action insertFencedCodeBlockAction = new Action( Messages.get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
475
      e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", Messages.get( "Main.menu.insert.fenced_code_block.prompt" ) ),
476
      activeFileEditorIsNull );
477
478
    Action insertLinkAction = new Action( Messages.get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
479
      e -> getActiveEditor().insertLink(),
480
      activeFileEditorIsNull );
481
    Action insertImageAction = new Action( Messages.get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT,
482
      e -> getActiveEditor().insertImage(),
483
      activeFileEditorIsNull );
484
485
    final Action[] headers = new Action[ 6 ];
486
487
    // Insert header actions (H1 ... H6)
488
    for( int i = 1; i <= 6; i++ ) {
489
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
490
      final String markup = String.format( "\n\n%s ", hashes );
491
      final String text = Messages.get( "Main.menu.insert.header_" + i );
492
      final String accelerator = "Shortcut+" + i;
493
      final String prompt = Messages.get( "Main.menu.insert.header_" + i + ".prompt" );
494
495
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
496
        e -> getActiveEditor().surroundSelection( markup, "", prompt ),
497
        activeFileEditorIsNull );
498
    }
499
500
    Action insertUnorderedListAction = new Action( Messages.get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
501
      e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
502
      activeFileEditorIsNull );
503
    Action insertOrderedListAction = new Action( Messages.get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
504
      e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
505
      activeFileEditorIsNull );
506
    Action insertHorizontalRuleAction = new Action( Messages.get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
507
      e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
508
      activeFileEditorIsNull );
509
510
    // Help actions
511
    Action helpAboutAction = new Action( Messages.get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
512
513
    //---- MenuBar ----
514
    Menu fileMenu = ActionUtils.createMenu( Messages.get( "Main.menu.file" ),
515
      fileNewAction,
516
      fileOpenAction,
517
      null,
518
      fileCloseAction,
519
      fileCloseAllAction,
520
      null,
521
      fileSaveAction,
522
      fileSaveAllAction,
523
      null,
524
      fileExitAction );
525
526
    Menu editMenu = ActionUtils.createMenu( Messages.get( "Main.menu.edit" ),
527
      editUndoAction,
528
      editRedoAction );
529
530
    Menu insertMenu = ActionUtils.createMenu( Messages.get( "Main.menu.insert" ),
531
      insertBoldAction,
532
      insertItalicAction,
533
      insertStrikethroughAction,
534
      insertBlockquoteAction,
535
      insertCodeAction,
536
      insertFencedCodeBlockAction,
537
      null,
538
      insertLinkAction,
539
      insertImageAction,
540
      null,
541
      headers[ 0 ],
542
      headers[ 1 ],
543
      headers[ 2 ],
544
      headers[ 3 ],
545
      headers[ 4 ],
546
      headers[ 5 ],
547
      null,
548
      insertUnorderedListAction,
549
      insertOrderedListAction,
550
      insertHorizontalRuleAction );
551
552
    Menu helpMenu = ActionUtils.createMenu( Messages.get( "Main.menu.help" ),
553
      helpAboutAction );
554
555
    menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu );
561556
562557
    //---- ToolBar ----
M src/main/java/com/scrivenvar/editor/EditorPane.java
3333
import javafx.application.Platform;
3434
import javafx.beans.property.ObjectProperty;
35
import javafx.beans.property.ReadOnlyDoubleProperty;
36
import javafx.beans.property.ReadOnlyDoubleWrapper;
3735
import javafx.beans.property.SimpleObjectProperty;
3836
import javafx.beans.value.ChangeListener;
...
5755
  private StyleClassedTextArea editor;
5856
  private VirtualizedScrollPane<StyleClassedTextArea> scrollPane;
59
  private final ReadOnlyDoubleWrapper scrollY = new ReadOnlyDoubleWrapper();
6057
  private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
61
62
  private String lineSeparator = getLineSeparator();
6358
6459
  /**
...
8580
8681
  public String getText() {
87
    String text = getEditor().getText();
88
89
    if( !this.lineSeparator.equals( "\n" ) ) {
90
      text = text.replace( "\n", this.lineSeparator );
91
    }
92
93
    return text;
82
    return getEditor().getText();
9483
  }
9584
9685
  public void setText( final String text ) {
97
    this.lineSeparator = determineLineSeparator( text );
9886
    getEditor().deselect();
9987
    getEditor().replaceText( text );
...
213201
  protected StyleClassedTextArea createTextArea() {
214202
    return new StyleClassedTextArea( false );
215
  }
216
217
  public double getScrollY() {
218
    return this.scrollY.get();
219
  }
220
221
  protected void setScrollY( double scrolled ) {
222
    this.scrollY.set( scrolled );
223
  }
224
225
  public ReadOnlyDoubleProperty scrollYProperty() {
226
    return this.scrollY.getReadOnlyProperty();
227203
  }
228204
...
237213
  public ObjectProperty<Path> pathProperty() {
238214
    return this.path;
239
  }
240
241
  private String getLineSeparator() {
242
    final String separator = getOptions().getLineSeparator();
243
244
    return (separator != null)
245
      ? separator
246
      : System.lineSeparator();
247
  }
248
249
  private String determineLineSeparator( final String s ) {
250
    final int length = s.length();
251
252
    // TODO: Looping backwards will probably detect a newline sooner.
253
    for( int i = 0; i < length; i++ ) {
254
      char ch = s.charAt( i );
255
      if( ch == '\n' ) {
256
        return (i > 0 && s.charAt( i - 1 ) == '\r') ? "\r\n" : "\n";
257
      }
258
    }
259
260
    return getLineSeparator();
261215
  }
262216
}
M src/main/java/com/scrivenvar/editor/MarkdownEditorPane.java
3232
import com.scrivenvar.dialogs.LinkDialog;
3333
import com.scrivenvar.processors.MarkdownProcessor;
34
import com.scrivenvar.util.Utils;
34
import static com.scrivenvar.util.Utils.ltrim;
35
import static com.scrivenvar.util.Utils.rtrim;
3536
import com.vladsch.flexmark.ast.Link;
3637
import com.vladsch.flexmark.ast.Node;
...
124125
    // remove leading whitespaces from leading text if selection starts at zero
125126
    if( start == 0 ) {
126
      leading = Utils.ltrim( leading );
127
      leading = ltrim( leading );
127128
    }
128129
129130
    // remove trailing whitespaces from trailing text if selection ends at text end
130131
    if( end == textArea.getLength() ) {
131
      trailing = Utils.rtrim( trailing );
132
      trailing = rtrim( trailing );
132133
    }
133134
M src/main/java/com/scrivenvar/editor/VariableNameInjector.java
6363
import static org.fxmisc.wellbehaved.event.InputMap.consume;
6464
import static org.fxmisc.wellbehaved.event.InputMap.sequence;
65
import static com.scrivenvar.definition.Lists.getFirst;
66
import static com.scrivenvar.definition.Lists.getLast;
67
import static java.lang.Character.isSpaceChar;
68
import static java.lang.Character.isWhitespace;
69
import static java.lang.Math.min;
70
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
71
import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
72
import static org.fxmisc.wellbehaved.event.InputMap.consume;
73
import static com.scrivenvar.definition.Lists.getFirst;
74
import static com.scrivenvar.definition.Lists.getLast;
75
import static java.lang.Character.isSpaceChar;
76
import static java.lang.Character.isWhitespace;
77
import static java.lang.Math.min;
78
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
79
import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
80
import static org.fxmisc.wellbehaved.event.InputMap.consume;
81
import static com.scrivenvar.definition.Lists.getFirst;
82
import static com.scrivenvar.definition.Lists.getLast;
83
import static java.lang.Character.isSpaceChar;
84
import static java.lang.Character.isWhitespace;
85
import static java.lang.Math.min;
86
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
87
import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
88
import static org.fxmisc.wellbehaved.event.InputMap.consume;
89
import static com.scrivenvar.definition.Lists.getFirst;
90
import static com.scrivenvar.definition.Lists.getLast;
91
import static java.lang.Character.isSpaceChar;
92
import static java.lang.Character.isWhitespace;
93
import static java.lang.Math.min;
94
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
95
import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
96
import static org.fxmisc.wellbehaved.event.InputMap.consume;
97
import static com.scrivenvar.definition.Lists.getFirst;
98
import static com.scrivenvar.definition.Lists.getLast;
99
import static java.lang.Character.isSpaceChar;
100
import static java.lang.Character.isWhitespace;
101
import static java.lang.Math.min;
102
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
103
import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
104
import static org.fxmisc.wellbehaved.event.InputMap.consume;
105
import static com.scrivenvar.definition.Lists.getFirst;
106
import static com.scrivenvar.definition.Lists.getLast;
107
import static java.lang.Character.isSpaceChar;
108
import static java.lang.Character.isWhitespace;
109
import static java.lang.Math.min;
110
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
111
import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
112
import static org.fxmisc.wellbehaved.event.InputMap.consume;
113
import static com.scrivenvar.definition.Lists.getFirst;
114
import static com.scrivenvar.definition.Lists.getLast;
115
import static java.lang.Character.isSpaceChar;
116
import static java.lang.Character.isWhitespace;
117
import static java.lang.Math.min;
118
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
119
import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
120
import static org.fxmisc.wellbehaved.event.InputMap.consume;
65121
66122
/**
D src/main/java/com/scrivenvar/options/GeneralOptionsPane.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.options;
28
29
import com.scrivenvar.Messages;
30
import com.scrivenvar.ui.AbstractPane;
31
import com.scrivenvar.util.Item;
32
import java.nio.charset.Charset;
33
import java.util.ArrayList;
34
import java.util.Collection;
35
import java.util.SortedMap;
36
import javafx.scene.control.ComboBox;
37
import javafx.scene.control.Label;
38
39
/**
40
 * General options pane.
41
 *
42
 * @author Karl Tauber
43
 */
44
public class GeneralOptionsPane extends AbstractPane {
45
46
  @SuppressWarnings( "unchecked" )
47
  public GeneralOptionsPane() {
48
    initComponents();
49
50
    String defaultLineSeparator = System.getProperty( "line.separator", "\n" );
51
    String defaultLineSeparatorStr = defaultLineSeparator.replace( "\r", "CR" ).replace( "\n", "LF" );
52
    lineSeparatorField.getItems().addAll(
53
      new Item<>( Messages.get( "GeneralOptionsPane.platformDefault", defaultLineSeparatorStr ), null ),
54
      new Item<>( Messages.get( "GeneralOptionsPane.sepWindows" ), "\r\n" ),
55
      new Item<>( Messages.get( "GeneralOptionsPane.sepUnix" ), "\n" ) );
56
57
    encodingField.getItems().addAll( getAvailableEncodings() );
58
  }
59
60
  private Collection<Item<String>> getAvailableEncodings() {
61
    SortedMap<String, Charset> availableCharsets = Charset.availableCharsets();
62
63
    ArrayList<Item<String>> values = new ArrayList<>( 1 + availableCharsets.size() );
64
    values.add( new Item<>( Messages.get( "GeneralOptionsPane.platformDefault", Charset.defaultCharset().name() ), null ) );
65
66
    for( String name : availableCharsets.keySet() ) {
67
      values.add( new Item<>( name, name ) );
68
    }
69
70
    return values;
71
  }
72
73
  void load() {
74
    lineSeparatorField.setValue( new Item<>( getOptions().getLineSeparator(), getOptions().getLineSeparator() ) );
75
    encodingField.setValue( new Item<>( getOptions().getEncoding(), getOptions().getEncoding() ) );
76
77
  }
78
79
  void save() {
80
    getOptions().setLineSeparator( lineSeparatorField.getValue().value );
81
    getOptions().setEncoding( encodingField.getValue().value );
82
83
  }
84
85
  private void initComponents() {
86
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
87
    Label lineSeparatorLabel = new Label();
88
    lineSeparatorField = new ComboBox<>();
89
    Label lineSeparatorLabel2 = new Label();
90
    Label encodingLabel = new Label();
91
    encodingField = new ComboBox<>();
92
93
    //======== this ========
94
    setCols( "[fill][fill][fill]" );
95
    setRows( "[][]para[]" );
96
97
    //---- lineSeparatorLabel ----
98
    lineSeparatorLabel.setText( Messages.get( "GeneralOptionsPane.lineSeparatorLabel.text" ) );
99
    lineSeparatorLabel.setMnemonicParsing( true );
100
    add( lineSeparatorLabel, "cell 0 0" );
101
    add( lineSeparatorField, "cell 1 0" );
102
103
    //---- lineSeparatorLabel2 ----
104
    lineSeparatorLabel2.setText( Messages.get( "GeneralOptionsPane.lineSeparatorLabel2.text" ) );
105
    add( lineSeparatorLabel2, "cell 2 0" );
106
107
    //---- encodingLabel ----
108
    encodingLabel.setText( Messages.get( "GeneralOptionsPane.encodingLabel.text" ) );
109
    encodingLabel.setMnemonicParsing( true );
110
    add( encodingLabel, "cell 0 1" );
111
112
    //---- encodingField ----
113
    encodingField.setVisibleRowCount( 20 );
114
    add( encodingField, "cell 1 1" );
115
		// JFormDesigner - End of component initialization  //GEN-END:initComponents
116
117
    // TODO set this in JFormDesigner as soon as it supports labelFor
118
    lineSeparatorLabel.setLabelFor( lineSeparatorField );
119
    encodingLabel.setLabelFor( encodingField );
120
  }
121
122
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
123
  private ComboBox<Item<String>> lineSeparatorField;
124
  private ComboBox<Item<String>> encodingField;
125
	// JFormDesigner - End of variables declaration  //GEN-END:variables
126
}
1271
D src/main/java/com/scrivenvar/options/GeneralOptionsPane.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "GeneralOptionsPane"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[fill][fill][fill]"
13
			"$rowConstraints": "[][]para[]"
14
		} ) {
15
			name: "this"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "lineSeparatorLabel"
18
				"text": new FormMessage( null, "GeneralOptionsPane.lineSeparatorLabel.text" )
19
				"mnemonicParsing": true
20
				auxiliary() {
21
					"JavaCodeGenerator.variableLocal": true
22
				}
23
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
24
				"value": "cell 0 0"
25
			} )
26
			add( new FormComponent( "javafx.scene.control.ComboBox" ) {
27
				name: "lineSeparatorField"
28
				auxiliary() {
29
					"JavaCodeGenerator.typeParameters": "Item<String>"
30
				}
31
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
32
				"value": "cell 1 0"
33
			} )
34
			add( new FormComponent( "javafx.scene.control.Label" ) {
35
				name: "lineSeparatorLabel2"
36
				"text": new FormMessage( null, "GeneralOptionsPane.lineSeparatorLabel2.text" )
37
				auxiliary() {
38
					"JavaCodeGenerator.variableLocal": true
39
				}
40
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
41
				"value": "cell 2 0"
42
			} )
43
			add( new FormComponent( "javafx.scene.control.Label" ) {
44
				name: "encodingLabel"
45
				"text": new FormMessage( null, "GeneralOptionsPane.encodingLabel.text" )
46
				"mnemonicParsing": true
47
				auxiliary() {
48
					"JavaCodeGenerator.variableLocal": true
49
				}
50
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
51
				"value": "cell 0 1"
52
			} )
53
			add( new FormComponent( "javafx.scene.control.ComboBox" ) {
54
				name: "encodingField"
55
				"visibleRowCount": 20
56
				auxiliary() {
57
					"JavaCodeGenerator.typeParameters": "Item<String>"
58
				}
59
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
60
				"value": "cell 1 1"
61
			} )
62
		}, new FormLayoutConstraints( null ) {
63
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
64
			"size": new javafx.geometry.Dimension2D( 400.0, 300.0 )
65
		} )
66
	}
67
}
681
D src/main/java/com/scrivenvar/options/MarkdownOptionsPane.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "MarkdownOptionsPane"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$rowConstraints": "[][][][][][][][][][][][][][][][][][]"
12
			"$columnConstraints": "[][fill]"
13
		} ) {
14
			name: "this"
15
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
16
				name: "smartsExtCheckBox"
17
				"text": new FormMessage( null, "MarkdownOptionsPane.smartsExtCheckBox.text" )
18
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
19
				"value": "cell 0 0"
20
			} )
21
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
22
				name: "quotesExtCheckBox"
23
				"text": new FormMessage( null, "MarkdownOptionsPane.quotesExtCheckBox.text" )
24
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
25
				"value": "cell 0 1"
26
			} )
27
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
28
				name: "abbreviationsExtCheckBox"
29
				"text": new FormMessage( null, "MarkdownOptionsPane.abbreviationsExtCheckBox.text" )
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 0 2"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) {
34
				name: "abbreviationsExtLink"
35
				"text": new FormMessage( null, "MarkdownOptionsPane.abbreviationsExtLink.text" )
36
				"uri": "http://michelf.com/projects/php-markdown/extra/#abbr"
37
				auxiliary() {
38
					"JavaCodeGenerator.variableLocal": true
39
				}
40
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
41
				"value": "cell 0 2,gapx 0"
42
			} )
43
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
44
				name: "hardwrapsExtCheckBox"
45
				"text": new FormMessage( null, "MarkdownOptionsPane.hardwrapsExtCheckBox.text" )
46
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
47
				"value": "cell 0 3"
48
			} )
49
			add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) {
50
				name: "hardwrapsExtLink"
51
				"text": new FormMessage( null, "MarkdownOptionsPane.hardwrapsExtLink.text" )
52
				"uri": "https://help.github.com/articles/writing-on-github/#markup"
53
				auxiliary() {
54
					"JavaCodeGenerator.variableLocal": true
55
				}
56
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
57
				"value": "cell 0 3,gapx 0"
58
			} )
59
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
60
				name: "autolinksExtCheckBox"
61
				"text": new FormMessage( null, "MarkdownOptionsPane.autolinksExtCheckBox.text" )
62
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
63
				"value": "cell 0 4"
64
			} )
65
			add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) {
66
				name: "autolinksExtLink"
67
				"text": new FormMessage( null, "MarkdownOptionsPane.autolinksExtLink.text" )
68
				"uri": "https://help.github.com/articles/github-flavored-markdown/#url-autolinking"
69
				auxiliary() {
70
					"JavaCodeGenerator.variableLocal": true
71
				}
72
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
73
				"value": "cell 0 4,gapx 0"
74
			} )
75
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
76
				name: "tablesExtCheckBox"
77
				"text": new FormMessage( null, "MarkdownOptionsPane.tablesExtCheckBox.text" )
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 0 5"
80
			} )
81
			add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) {
82
				name: "tablesExtLink"
83
				"text": new FormMessage( null, "MarkdownOptionsPane.tablesExtLink.text" )
84
				"uri": "http://fletcher.github.io/MultiMarkdown-4/syntax.html#tables"
85
				auxiliary() {
86
					"JavaCodeGenerator.variableLocal": true
87
				}
88
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
89
				"value": "cell 0 5,gapx 0"
90
			} )
91
			add( new FormComponent( "javafx.scene.control.Label" ) {
92
				name: "tablesExtLabel"
93
				"text": new FormMessage( null, "MarkdownOptionsPane.tablesExtLabel.text" )
94
				auxiliary() {
95
					"JavaCodeGenerator.variableLocal": true
96
				}
97
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
98
				"value": "cell 0 5,gapx 3"
99
			} )
100
			add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) {
101
				name: "tablesExtLink2"
102
				"text": new FormMessage( null, "MarkdownOptionsPane.tablesExtLink2.text" )
103
				"uri": "https://michelf.ca/projects/php-markdown/extra/#table"
104
				auxiliary() {
105
					"JavaCodeGenerator.variableLocal": true
106
				}
107
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
108
				"value": "cell 0 5,gapx 3 3"
109
			} )
110
			add( new FormComponent( "javafx.scene.control.Label" ) {
111
				name: "tablesExtLabel2"
112
				"text": new FormMessage( null, "MarkdownOptionsPane.tablesExtLabel2.text" )
113
				auxiliary() {
114
					"JavaCodeGenerator.variableLocal": true
115
				}
116
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
117
				"value": "cell 0 5,gapx 0"
118
			} )
119
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
120
				name: "definitionListsExtCheckBox"
121
				"text": new FormMessage( null, "MarkdownOptionsPane.definitionListsExtCheckBox.text" )
122
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
123
				"value": "cell 0 6"
124
			} )
125
			add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) {
126
				name: "definitionListsExtLink"
127
				"text": new FormMessage( null, "MarkdownOptionsPane.definitionListsExtLink.text" )
128
				"uri": "https://michelf.ca/projects/php-markdown/extra/#def-list"
129
				auxiliary() {
130
					"JavaCodeGenerator.variableLocal": true
131
				}
132
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
133
				"value": "cell 0 6,gapx 0"
134
			} )
135
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
136
				name: "fencedCodeBlocksExtCheckBox"
137
				"text": new FormMessage( null, "MarkdownOptionsPane.fencedCodeBlocksExtCheckBox.text" )
138
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
139
				"value": "cell 0 7"
140
			} )
141
			add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) {
142
				name: "fencedCodeBlocksExtLink"
143
				"text": new FormMessage( null, "MarkdownOptionsPane.fencedCodeBlocksExtLink.text" )
144
				"uri": "http://michelf.com/projects/php-markdown/extra/#fenced-code-blocks"
145
				auxiliary() {
146
					"JavaCodeGenerator.variableLocal": true
147
				}
148
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
149
				"value": "cell 0 7,gapx 0"
150
			} )
151
			add( new FormComponent( "javafx.scene.control.Label" ) {
152
				name: "fencedCodeBlocksExtLabel"
153
				"text": new FormMessage( null, "MarkdownOptionsPane.fencedCodeBlocksExtLabel.text" )
154
				auxiliary() {
155
					"JavaCodeGenerator.variableLocal": true
156
				}
157
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
158
				"value": "cell 0 7,gapx 3"
159
			} )
160
			add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) {
161
				name: "fencedCodeBlocksExtLink2"
162
				"text": new FormMessage( null, "MarkdownOptionsPane.fencedCodeBlocksExtLink2.text" )
163
				"uri": "https://help.github.com/articles/github-flavored-markdown/#fenced-code-blocks"
164
				auxiliary() {
165
					"JavaCodeGenerator.variableLocal": true
166
				}
167
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
168
				"value": "cell 0 7,gapx 3"
169
			} )
170
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
171
				name: "wikilinksExtCheckBox"
172
				"text": new FormMessage( null, "MarkdownOptionsPane.wikilinksExtCheckBox.text" )
173
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
174
				"value": "cell 0 8"
175
			} )
176
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
177
				name: "strikethroughExtCheckBox"
178
				"text": new FormMessage( null, "MarkdownOptionsPane.strikethroughExtCheckBox.text" )
179
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
180
				"value": "cell 0 9"
181
			} )
182
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
183
				name: "anchorlinksExtCheckBox"
184
				"text": new FormMessage( null, "MarkdownOptionsPane.anchorlinksExtCheckBox.text" )
185
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
186
				"value": "cell 0 10"
187
			} )
188
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
189
				name: "suppressHtmlBlocksExtCheckBox"
190
				"text": new FormMessage( null, "MarkdownOptionsPane.suppressHtmlBlocksExtCheckBox.text" )
191
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
192
				"value": "cell 0 11"
193
			} )
194
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
195
				name: "suppressInlineHtmlExtCheckBox"
196
				"text": new FormMessage( null, "MarkdownOptionsPane.suppressInlineHtmlExtCheckBox.text" )
197
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
198
				"value": "cell 0 12"
199
			} )
200
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
201
				name: "atxHeaderSpaceExtCheckBox"
202
				"text": new FormMessage( null, "MarkdownOptionsPane.atxHeaderSpaceExtCheckBox.text" )
203
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
204
				"value": "cell 0 13"
205
			} )
206
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
207
				name: "forceListItemParaExtCheckBox"
208
				"text": new FormMessage( null, "MarkdownOptionsPane.forceListItemParaExtCheckBox.text" )
209
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
210
				"value": "cell 0 14"
211
			} )
212
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
213
				name: "relaxedHrRulesExtCheckBox"
214
				"text": new FormMessage( null, "MarkdownOptionsPane.relaxedHrRulesExtCheckBox.text" )
215
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
216
				"value": "cell 0 15"
217
			} )
218
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
219
				name: "taskListItemsExtCheckBox"
220
				"text": new FormMessage( null, "MarkdownOptionsPane.taskListItemsExtCheckBox.text" )
221
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
222
				"value": "cell 0 16"
223
			} )
224
			add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) {
225
				name: "extAnchorLinksExtCheckBox"
226
				"text": new FormMessage( null, "MarkdownOptionsPane.extAnchorLinksExtCheckBox.text" )
227
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
228
				"value": "cell 0 17"
229
			} )
230
		}, new FormLayoutConstraints( null ) {
231
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
232
			"size": new javafx.geometry.Dimension2D( 600.0, 531.0 )
233
		} )
234
	}
235
}
2361
D src/main/java/com/scrivenvar/options/OptionsDialog.java
1
/*
2
 * Copyright 2016 Karl Tauber 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.options;
29
30
import com.scrivenvar.Messages;
31
import com.scrivenvar.Services;
32
import com.scrivenvar.service.Options;
33
import com.scrivenvar.service.events.impl.ButtonOrderPane;
34
import java.util.prefs.Preferences;
35
import javafx.event.ActionEvent;
36
import javafx.scene.control.ButtonType;
37
import javafx.scene.control.Dialog;
38
import javafx.scene.control.DialogPane;
39
import javafx.scene.control.Tab;
40
import javafx.scene.control.TabPane;
41
import javafx.stage.Window;
42
43
/**
44
 * Options dialog.
45
 *
46
 * @author Karl Tauber and White Magic Software, Ltd.
47
 */
48
public class OptionsDialog extends Dialog<Void> {
49
50
  private final Options options = Services.load( Options.class );
51
52
  public OptionsDialog( Window owner ) {
53
    setTitle( Messages.get( "OptionsDialog.title" ) );
54
    initOwner( owner );
55
56
    initComponents();
57
58
    tabPane.getStyleClass().add( TabPane.STYLE_CLASS_FLOATING );
59
60
    setDialogPane( new ButtonOrderPane() );
61
62
    final DialogPane dialogPane = getDialogPane();
63
    dialogPane.setContent( tabPane );
64
    dialogPane.getButtonTypes().addAll( ButtonType.OK, ButtonType.CANCEL );
65
66
    dialogPane.lookupButton( ButtonType.OK ).addEventHandler( ActionEvent.ACTION, e -> {
67
      save();
68
      e.consume();
69
    } );
70
71
    // load options
72
    load();
73
74
    // select last tab
75
    int tabIndex = getState().getInt( "lastOptionsTab", -1 );
76
    if( tabIndex > 0 ) {
77
      tabPane.getSelectionModel().select( tabIndex );
78
    }
79
80
    // remember last selected tab
81
    setOnHidden( e -> {
82
      getState().putInt( "lastOptionsTab", tabPane.getSelectionModel().getSelectedIndex() );
83
    } );
84
  }
85
86
  private Options getOptions() {
87
    return options;
88
  }
89
  
90
  private Preferences getState() {
91
    return getOptions().getState();
92
  }
93
94
  private void load() {
95
    generalOptionsPane.load();
96
  }
97
98
  private void save() {
99
    generalOptionsPane.save();
100
    Services.load( Options.class ).save();
101
  }
102
103
  private void initComponents() {
104
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
105
    tabPane = new TabPane();
106
    generalTab = new Tab();
107
    generalOptionsPane = new GeneralOptionsPane();
108
109
    //======== tabPane ========
110
    {
111
      tabPane.setTabClosingPolicy( TabPane.TabClosingPolicy.UNAVAILABLE );
112
113
      //======== generalTab ========
114
      {
115
        generalTab.setText( Messages.get( "OptionsDialog.generalTab.text" ) );
116
        generalTab.setContent( generalOptionsPane );
117
      }
118
119
      tabPane.getTabs().addAll( generalTab );
120
    }
121
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
122
  }
123
124
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
125
  private TabPane tabPane;
126
  private Tab generalTab;
127
  private GeneralOptionsPane generalOptionsPane;
128
	// JFormDesigner - End of variables declaration  //GEN-END:variables
129
}
1301
D src/main/java/com/scrivenvar/options/OptionsDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.keyPrefix": "OptionsDialog"
7
	"i18n.autoExternalize": true
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "javafx.scene.control.TabPane", new FormLayoutManager( class javafx.scene.control.TabPane ) ) {
11
			name: "tabPane"
12
			"tabClosingPolicy": enum javafx.scene.control.TabPane$TabClosingPolicy UNAVAILABLE
13
			add( new FormContainer( "javafx.scene.control.Tab", new FormLayoutManager( class javafx.scene.control.Tab ) ) {
14
				name: "generalTab"
15
				"text": new FormMessage( null, "OptionsDialog.generalTab.text" )
16
				add( new FormComponent( "com.scrivendor.options.GeneralOptionsPane" ) {
17
					name: "generalOptionsPane"
18
				} )
19
			} )
20
			add( new FormContainer( "javafx.scene.control.Tab", new FormLayoutManager( class javafx.scene.control.Tab ) ) {
21
				name: "markdownTab"
22
				"text": new FormMessage( null, "OptionsDialog.markdownTab.text" )
23
				add( new FormComponent( "com.scrivendor.options.MarkdownOptionsPane" ) {
24
					name: "markdownOptionsPane"
25
				} )
26
			} )
27
		}, new FormLayoutConstraints( null ) {
28
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
29
			"size": new javafx.geometry.Dimension2D( 600.0, 560.0 )
30
		} )
31
	}
32
}
331
M src/main/java/com/scrivenvar/preview/HTMLPreviewPane.java
3333
import javafx.concurrent.Worker.State;
3434
import static javafx.concurrent.Worker.State.SUCCEEDED;
35
import javafx.scene.Node;
3536
import javafx.scene.layout.Pane;
3637
import javafx.scene.web.WebEngine;
...
5758
    setPath( path );
5859
    initListeners();
60
    initTraversal();
61
  }
5962
60
    // Prevent tabbing into the preview pane.
61
    getWebView().setFocusTraversable( false );
63
  /**
64
   * Initializes observers for document changes. When the document is reloaded
65
   * with new HTML, this triggers a scroll event that repositions the document
66
   * to the injected caret (that corresponds with the position in the text
67
   * editor).
68
   */
69
  private void initListeners() {
70
    // Scrolls to the caret after the content has been loaded.
71
    getEngine().getLoadWorker().stateProperty().addListener(
72
      (ObservableValue<? extends State> observable,
73
        final State oldValue, final State newValue) -> {
74
        if( newValue == SUCCEEDED ) {
75
          scrollToCaret();
76
        }
77
      } );
78
  }
79
80
  /**
81
   * Ensures images can be found relative to the document.
82
   *
83
   * @return The base path element to use for the document, or the empty string
84
   * if no path has been set, yet.
85
   */
86
  private String getBase() {
87
    final Path basePath = getPath();
88
89
    return basePath == null
90
      ? ""
91
      : ("<base href='" + basePath.getParent().toUri().toString() + "'>");
6292
  }
6393
6494
  /**
6595
   * Updates the internal HTML source, loads it into the preview pane, then
6696
   * scrolls to the caret position.
6797
   *
68
   * @param html
98
   * @param html The new HTML document to display.
6999
   */
70100
  public void update( final String html ) {
71
    setHtml( html );
72
    update();
73
  }
74
75
  private void update() {
76101
    getEngine().loadContent(
77102
      "<!DOCTYPE html>"
78103
      + "<html>"
79104
      + "<head>"
80105
      + "<link rel='stylesheet' href='" + getClass().getResource( "webview.css" ) + "'>"
81106
      + getBase()
82107
      + "</head>"
83108
      + "<body>"
84
      + getHtml()
109
      + html
85110
      + "</body>"
86111
      + "</html>" );
87
  }
88
89
  private String getBase() {
90
    final Path basePath = getPath();
91
92
    return basePath == null
93
      ? ""
94
      : ("<base href='" + basePath.getParent().toUri().toString() + "'>");
95
  }
96
97
  /**
98
   * Initializes observers for document changes. When the document is reloaded
99
   * with new HTML, this triggers a scroll event that repositions the document
100
   * to the injected caret (that corresponds with the position in the text
101
   * editor).
102
   */
103
  private void initListeners() {
104
    // Scrolls to the caret after the content has been loaded.
105
    getEngine().getLoadWorker().stateProperty().addListener(
106
      (ObservableValue<? extends State> observable,
107
        State oldValue, State newValue) -> {
108
        if( newValue == SUCCEEDED ) {
109
          scrollToCaret();
110
        }
111
      } );
112112
  }
113113
...
133133
      + "  window.scrollTo( 0, e.topOffset() - (window.innerHeight / 2 ) );"
134134
      + "}";
135
  }
136
137
  /**
138
   * Prevent tabbing into the preview pane.
139
   */
140
  private void initTraversal() {
141
    getWebView().setFocusTraversable( false );
135142
  }
136143
...
143150
  }
144151
145
  public WebView getWebView() {
152
  private WebView getWebView() {
146153
    return this.webView;
147
  }
148
149
  private String getHtml() {
150
    return this.html;
151
  }
152
153
  private void setHtml( final String html ) {
154
    this.html = html;
155154
  }
156155
157156
  private Path getPath() {
158157
    return this.path;
159158
  }
160159
161160
  private void setPath( final Path path ) {
162161
    this.path = path;
162
  }
163
  
164
  public Node getNode() {
165
    return getWebView();
163166
  }
164167
}
M src/main/java/com/scrivenvar/service/Options.java
2929
3030
import java.util.prefs.Preferences;
31
import javafx.beans.property.StringProperty;
3231
3332
/**
34
 * Options
33
 * Responsible for persistent options.
3534
 *
3635
 * @author White Magic Software, Ltd.
3736
 */
3837
public interface Options {
39
  
40
  public Preferences getState();
41
42
  public void load( Preferences options );
43
44
  public void save();
45
46
  public String getLineSeparator();
47
48
  public void setLineSeparator( String lineSeparator );
49
50
  public StringProperty lineSeparatorProperty();
51
52
  public String getEncoding();
5338
54
  public void setEncoding( String encoding );
39
  public Preferences getState();
40
  
41
  /**
42
   * Stores the key and value into the user preferences to be loaded the next
43
   * time the application is launched.
44
   *
45
   * @param key Name of the key to persist along with its value.
46
   * @param value Value to associate with the key.
47
   */
48
  public void put( String key, String value );
5549
56
  public StringProperty encodingProperty();
50
  /**
51
   * Retrieves the value for a key in the user preferences.
52
   *
53
   * @param key Retrieve the value of this key.
54
   * @param defaultValue The value to return in the event that the given key has
55
   * no associated value.
56
   *
57
   * @return The value associated with the key.
58
   */
59
  public String get( String key, String defaultValue );
5760
}
5861
M src/main/java/com/scrivenvar/service/impl/DefaultOptions.java
2828
2929
import com.scrivenvar.service.Options;
30
import static com.scrivenvar.util.Utils.putPrefs;
3130
import java.util.prefs.Preferences;
3231
import static java.util.prefs.Preferences.userRoot;
33
import javafx.beans.property.SimpleStringProperty;
34
import javafx.beans.property.StringProperty;
3532
3633
/**
3734
 * Persistent options user can change at runtime.
3835
 *
3936
 * @author Karl Tauber and White Magic Software, Ltd.
4037
 */
4138
public class DefaultOptions implements Options {
42
  private final StringProperty LINE_SEPARATOR = new SimpleStringProperty();
43
  private final StringProperty ENCODING = new SimpleStringProperty();
44
4539
  private Preferences preferences;
4640
  
4741
  public DefaultOptions() {
4842
    setPreferences( getRootPreferences().node( "options" ) );
43
  }
44
45
  @Override
46
  public void put( final String key, final String value ) {
47
    getPreferences().put( key, value );
4948
  }
5049
  
51
  private void setPreferences( Preferences preferences ) {
50
  @Override
51
  public String get( final String key, final String defalutValue ) {
52
    return getPreferences().get( key, defalutValue );
53
  }
54
  
55
  private void setPreferences( final Preferences preferences ) {
5256
    this.preferences = preferences;
5357
  }
...
6266
  }
6367
64
  public Preferences getPreferences() {
68
  private Preferences getPreferences() {
6569
    return this.preferences;
66
  }
67
68
  @Override
69
  public void load( Preferences options ) {
70
    setLineSeparator( options.get( "lineSeparator", null ) );
71
    setEncoding( options.get( "encoding", null ) );
72
  }
73
74
  @Override
75
  public void save() {
76
    final Preferences prefs = getPreferences();
77
    
78
    putPrefs( prefs, "lineSeparator", getLineSeparator(), null );
79
    putPrefs( prefs, "encoding", getEncoding(), null );
80
  }
81
82
  @Override
83
  public String getLineSeparator() {
84
    return LINE_SEPARATOR.get();
85
  }
86
87
  @Override
88
  public void setLineSeparator( String lineSeparator ) {
89
    LINE_SEPARATOR.set( lineSeparator );
90
  }
91
92
  @Override
93
  public StringProperty lineSeparatorProperty() {
94
    return LINE_SEPARATOR;
95
  }
96
97
  @Override
98
  public String getEncoding() {
99
    return ENCODING.get();
100
  }
101
102
  @Override
103
  public void setEncoding( String encoding ) {
104
    ENCODING.set( encoding );
105
  }
106
107
  @Override
108
  public StringProperty encodingProperty() {
109
    return ENCODING;
11070
  }
11171
}
M src/main/java/com/scrivenvar/util/Item.java
2525
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2626
 */
27
2827
package com.scrivenvar.util;
2928
3029
/**
31
 * Simple item for a ChoiceBox, ComboBox or ListView.
32
 * Consists of a string name and a value object.
33
 * toString() returns the name.
34
 * equals() compares the value and hashCode() returns the hash code of the value.
30
 * Simple item for a ChoiceBox, ComboBox or ListView. Consists of a string name
31
 * and a value object. toString() returns the name. equals() compares the value
32
 * and hashCode() returns the hash code of the value.
3533
 *
3634
 * @author Karl Tauber
35
 * @param <V> The type of item value.
3736
 */
38
public class Item<V>
39
{
40
	public final String name;
41
	public final V value;
37
public class Item<V> {
4238
43
	public Item(String name, V value) {
44
		this.name = name;
45
		this.value = value;
46
	}
39
  public final String name;
40
  public final V value;
4741
48
	@Override
49
	public boolean equals(Object obj) {
50
		if (this == obj)
51
			return true;
52
		if (!(obj instanceof Item))
53
			return false;
54
		return Utils.safeEquals(value, ((Item<?>)obj).value);
55
	}
42
  public Item( final String name, final V value ) {
43
    this.name = name;
44
    this.value = value;
45
  }
5646
57
	@Override
58
	public int hashCode() {
59
		return (value != null) ? value.hashCode() : 0;
60
	}
47
  @Override
48
  public boolean equals( final Object obj ) {
49
    if( this == obj ) {
50
      return true;
51
    }
52
    if( !(obj instanceof Item) ) {
53
      return false;
54
    }
55
    return Utils.safeEquals( value, ((Item<?>)obj).value );
56
  }
6157
62
	@Override
63
	public String toString() {
64
		return name;
65
	}
58
  @Override
59
  public int hashCode() {
60
    return (value != null) ? value.hashCode() : 0;
61
  }
62
63
  @Override
64
  public String toString() {
65
    return name;
66
  }
6667
}
6768
M src/main/java/com/scrivenvar/util/Utils.java
2828
2929
import java.util.ArrayList;
30
import java.util.Set;
3130
import java.util.prefs.Preferences;
32
import javafx.geometry.Orientation;
33
import javafx.scene.Node;
34
import javafx.scene.control.ScrollBar;
3531
3632
/**
3733
 * @author Karl Tauber
3834
 */
3935
public class Utils {
4036
41
  public static boolean safeEquals( Object o1, Object o2 ) {
37
  public static boolean safeEquals( final Object o1, final Object o2 ) {
4238
    if( o1 == o2 ) {
4339
      return true;
4440
    }
4541
    if( o1 == null || o2 == null ) {
4642
      return false;
4743
    }
4844
    return o1.equals( o2 );
4945
  }
5046
51
  public static boolean isNullOrEmpty( String s ) {
47
  public static boolean isNullOrEmpty( final String s ) {
5248
    return s == null || s.isEmpty();
5349
  }
...
9894
9995
  public static String[] getPrefsStrings( final Preferences prefs, String key ) {
100
    final ArrayList<String> arr = new ArrayList<>();
96
    final ArrayList<String> arr = new ArrayList<>( 256 );
10197
10298
    for( int i = 0; i < 10000; i++ ) {
10399
      final String s = prefs.get( key + (i + 1), null );
104100
105101
      if( s == null ) {
106102
        break;
107103
      }
104
108105
      arr.add( s );
109106
    }
...
119116
    for( int i = strings.length; prefs.get( key + (i + 1), null ) != null; i++ ) {
120117
      prefs.remove( key + (i + 1) );
121
    }
122
  }
123
124
  public static ScrollBar findVScrollBar( Node node ) {
125
    final Set<Node> scrollBars = node.lookupAll( ".scroll-bar" );
126
127
    for( final Node scrollBar : scrollBars ) {
128
      if( scrollBar instanceof ScrollBar
129
        && ((ScrollBar)scrollBar).getOrientation() == Orientation.VERTICAL ) {
130
        return (ScrollBar)scrollBar;
131
      }
132118
    }
133
134
    return null;
135119
  }
136120
}
A src/main/resources/com/scrivenvar/build.sh
1
#!/bin/bash
2
3
INKSCAPE=/usr/bin/inkscape
4
5
declare -a SIZES=("16" "32" "64" "128" "256" "512")
6
7
for i in "${SIZES[@]}"; do
8
  # -y: export background opacity 0
9
  $INKSCAPE -y 0 -z -f "logo.svg" -w "${i}" -e "logo${i}.png"
10
done
11
112