Dave Jarvis' Repositories

M CHANGES.md
11
# Change Log
22
3
## 0.7
3
## 0.8
44
55
- Load YAML variables from files
6
- Upgraded to Apache Commons Configuration 2.1
7
- Fixed bug with settings using comma-separated file extensions
8
9
## 0.7
10
611
- Added cursor to the preview pane
712
- Reconfigured constants to use settings
M README.md
11
![Logo](images/logo64.png)
22
3
Scrivenvar
3
$application.title$
44
===
55
M build.gradle
3030
  compile group: 'org.yaml', name: 'snakeyaml', version: '1.17'
3131
  compile group: 'com.googlecode.juniversalchardet', name: 'juniversalchardet', version: '1.0.3'
32
  compile group: 'commons-configuration', name: 'commons-configuration', version: '1.10'
32
  compile group: 'org.apache.commons', name: 'commons-configuration2', version: '2.1'
3333
}
3434
A scrivenvar.yaml
1
application:
2
  title: Scrivenvar
3
14
M src/main/java/com/scrivenvar/Constants.java
6868
6969
  public static final String PREFS_ROOT = get( "preferences.root" );
70
  public static final String PREFS_ROOT_STATE = get( "preferences.root.state" );
71
  public static final String PREFS_ROOT_OPTIONS = get( "preferences.root.options" );
70
  public static final String PREFS_STATE = get( "preferences.root.state" );
71
  public static final String PREFS_OPTIONS = get( "preferences.root.options" );
72
  public static final String PREFS_DEFINITION_SOURCE = get( "preferences.root.definition.source" );
7273
}
7374
M src/main/java/com/scrivenvar/FileEditorTabPane.java
2828
package com.scrivenvar;
2929
30
import com.scrivenvar.predicates.files.FileTypePredicate;
31
import com.scrivenvar.service.Options;
32
import com.scrivenvar.service.Settings;
33
import com.scrivenvar.service.events.AlertMessage;
34
import com.scrivenvar.service.events.AlertService;
35
import static com.scrivenvar.service.events.AlertService.NO;
36
import static com.scrivenvar.service.events.AlertService.YES;
37
import com.scrivenvar.util.Utils;
38
import java.io.File;
39
import java.nio.file.Path;
40
import java.util.ArrayList;
41
import java.util.List;
42
import java.util.function.Consumer;
43
import java.util.prefs.Preferences;
44
import java.util.stream.Collectors;
45
import javafx.beans.property.ReadOnlyBooleanProperty;
46
import javafx.beans.property.ReadOnlyBooleanWrapper;
47
import javafx.beans.property.ReadOnlyObjectProperty;
48
import javafx.beans.property.ReadOnlyObjectWrapper;
49
import javafx.beans.value.ChangeListener;
50
import javafx.beans.value.ObservableValue;
51
import javafx.collections.ListChangeListener;
52
import javafx.collections.ObservableList;
53
import javafx.event.Event;
54
import javafx.scene.Node;
55
import javafx.scene.control.Alert;
56
import javafx.scene.control.ButtonType;
57
import javafx.scene.control.Tab;
58
import javafx.scene.control.TabPane;
59
import javafx.scene.control.TabPane.TabClosingPolicy;
60
import javafx.scene.input.InputEvent;
61
import javafx.stage.FileChooser;
62
import javafx.stage.FileChooser.ExtensionFilter;
63
import javafx.stage.Window;
64
import org.fxmisc.richtext.StyledTextArea;
65
import org.fxmisc.wellbehaved.event.EventPattern;
66
import org.fxmisc.wellbehaved.event.InputMap;
67
import static com.scrivenvar.Messages.get;
68
69
/**
70
 * Tab pane for file editors.
71
 *
72
 * @author Karl Tauber and White Magic Software, Ltd.
73
 */
74
public final class FileEditorTabPane extends TabPane {
75
76
  private final static String FILTER_EXTENSIONS = "filter.file";
77
  private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose.filter";
78
79
  private final Options options = Services.load( Options.class );
80
  private final Settings settings = Services.load( Settings.class );
81
  private final AlertService alertService = Services.load( AlertService.class );
82
83
  private final ReadOnlyObjectWrapper<Path> openDefinition = new ReadOnlyObjectWrapper<>();
84
  private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>();
85
  private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper();
86
87
  /**
88
   * Constructs a new file editor tab pane.
89
   */
90
  public FileEditorTabPane() {
91
    final ObservableList<Tab> tabs = getTabs();
92
93
    setFocusTraversable( false );
94
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
95
96
    addTabSelectionListener(
97
      (ObservableValue<? extends Tab> tabPane,
98
        final Tab oldTab, final Tab newTab) -> {
99
100
        if( newTab != null ) {
101
          activeFileEditor.set( (FileEditorTab)newTab );
102
        }
103
      }
104
    );
105
106
    final ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> {
107
      for( final Tab tab : tabs ) {
108
        if( ((FileEditorTab)tab).isModified() ) {
109
          this.anyFileEditorModified.set( true );
110
          break;
111
        }
112
      }
113
    };
114
115
    tabs.addListener(
116
      (ListChangeListener<Tab>)change -> {
117
        while( change.next() ) {
118
          if( change.wasAdded() ) {
119
            change.getAddedSubList().stream().forEach( (tab) -> {
120
              ((FileEditorTab)tab).modifiedProperty().addListener( modifiedListener );
121
            } );
122
          } else if( change.wasRemoved() ) {
123
            change.getRemoved().stream().forEach( (tab) -> {
124
              ((FileEditorTab)tab).modifiedProperty().removeListener( modifiedListener );
125
            } );
126
          }
127
        }
128
129
        // Changes in the tabs may also change anyFileEditorModified property
130
        // (e.g. closed modified file)
131
        modifiedListener.changed( null, null, null );
132
      }
133
    );
134
  }
135
136
  /**
137
   * Delegates to the active file editor.
138
   *
139
   * @param <T> Event type.
140
   * @param <U> Consumer type.
141
   * @param event Event to pass to the editor.
142
   * @param consumer Consumer to pass to the editor.
143
   */
144
  public <T extends Event, U extends T> void addEventListener(
145
    final EventPattern<? super T, ? extends U> event,
146
    final Consumer<? super U> consumer ) {
147
    getActiveFileEditor().addEventListener( event, consumer );
148
  }
149
150
  /**
151
   * Delegates to the active file editor pane, and, ultimately, to its text
152
   * area.
153
   *
154
   * @param map The map of methods to events.
155
   */
156
  public void addEventListener( final InputMap<InputEvent> map ) {
157
    getActiveFileEditor().addEventListener( map );
158
  }
159
160
  /**
161
   * Remove a keyboard event listener from the active file editor.
162
   *
163
   * @param map The keyboard events to remove.
164
   */
165
  public void removeEventListener( final InputMap<InputEvent> map ) {
166
    getActiveFileEditor().removeEventListener( map );
167
  }
168
169
  /**
170
   * Allows observers to be notified when the current file editor tab changes.
171
   *
172
   * @param listener The listener to notify of tab change events.
173
   */
174
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
175
    // Observe the tab so that when a new tab is opened or selected,
176
    // a notification is kicked off.
177
    getSelectionModel().selectedItemProperty().addListener( listener );
178
  }
179
180
  /**
181
   * Allows clients to manipulate the editor content directly.
182
   *
183
   * @return The text area for the active file editor.
184
   */
185
  public StyledTextArea getEditor() {
186
    return getActiveFileEditor().getEditorPane().getEditor();
187
  }
188
189
  public FileEditorTab getActiveFileEditor() {
190
    return this.activeFileEditor.get();
191
  }
192
193
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
194
    return this.activeFileEditor.getReadOnlyProperty();
195
  }
196
197
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
198
    return this.anyFileEditorModified.getReadOnlyProperty();
199
  }
200
201
  private FileEditorTab createFileEditor( final Path path ) {
202
    final FileEditorTab tab = new FileEditorTab( path );
203
204
    tab.setOnCloseRequest( e -> {
205
      if( !canCloseEditor( tab ) ) {
206
        e.consume();
207
      }
208
    } );
209
210
    return tab;
211
  }
212
213
  /**
214
   * Called when the user selects New from the File menu.
215
   *
216
   * @return The newly added tab.
217
   */
218
  void newEditor() {
219
    final FileEditorTab tab = createFileEditor( null );
220
221
    getTabs().add( tab );
222
    getSelectionModel().select( tab );
223
  }
224
225
  void openFileDialog() {
226
    final String title = get( "Dialog.file.choose.open.title" );
227
    final FileChooser dialog = createFileChooser( title );
228
    openFiles( dialog.showOpenMultipleDialog( getWindow() ) );
229
  }
230
231
  /**
232
   * Opens the files into new editors, unless one of those files was a
233
   * definition file. The definition file is loaded into the definition pane,
234
   * but only the first one selected (multiple definition files will result in a
235
   * warning).
236
   *
237
   * @param files The list of non-definition files that the were requested to
238
   * open.
239
   *
240
   * @return A list of files that can be opened in text editors.
241
   */
242
  private void openFiles( final List<File> files ) {
243
    final FileTypePredicate predicate
244
      = new FileTypePredicate( createExtensionFilter( "definition" ).getExtensions() );
245
246
    // The user might have opened multiple definitions files. These will
247
    // be discarded from the text editable files.
248
    final List<File> definitions
249
      = files.stream().filter( predicate ).collect( Collectors.toList() );
250
251
    // Create a modifiable list to remove any definition files that were
252
    // opened.
253
    final List<File> editors = new ArrayList<>( files );
254
255
    if( editors.size() > 0 ) {
256
      saveLastDirectory( editors.get( 0 ) );
257
    }
258
259
    editors.removeAll( definitions );
260
261
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
262
    if( editors.size() > 0 ) {
263
      openEditors( editors, 0 );
264
    }
265
266
    if( definitions.size() > 0 ) {
267
      openDefinition( definitions.get( 0 ) );
268
    }
269
  }
270
271
  private void openEditors( final List<File> files, final int activeIndex ) {
272
    final int fileTally = files.size();
273
    final List<Tab> tabs = getTabs();
274
275
    // Close single unmodified "Untitled" tab.
276
    if( tabs.size() == 1 ) {
277
      final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 ));
278
279
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
280
        closeEditor( fileEditor, false );
281
      }
282
    }
283
284
    for( int i = 0; i < fileTally; i++ ) {
285
      final Path path = files.get( i ).toPath();
286
287
      FileEditorTab fileEditorTab = findEditor( path );
288
289
      // Only open new files.
290
      if( fileEditorTab == null ) {
291
        fileEditorTab = createFileEditor( path );
292
        getTabs().add( fileEditorTab );
293
      }
294
295
      // Select the first file in the list.
296
      if( i == activeIndex ) {
297
        getSelectionModel().select( fileEditorTab );
298
      }
299
    }
300
  }
301
302
  /**
303
   * Returns a property that changes when a new definition file is opened.
304
   *
305
   * @return The path to a definition file that was opened.
306
   */
307
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
308
    return getOnOpenDefinitionFile().getReadOnlyProperty();
309
  }
310
311
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
312
    return this.openDefinition;
313
  }
314
315
  /**
316
   * Called when the user has opened a definition file (using the file open
317
   * dialog box). This will replace the current set of definitions for the
318
   * active tab.
319
   *
320
   * @param definition The file to open.
321
   */
322
  private void openDefinition( final File definition ) {
323
    // TODO: Prevent reading this file twice when a new text document is opened.
324
    // (might be a matter of checking the value first).
325
    getOnOpenDefinitionFile().set( definition.toPath() );
326
  }
327
328
  boolean saveEditor( final FileEditorTab fileEditor ) {
329
    if( fileEditor == null || !fileEditor.isModified() ) {
330
      return true;
331
    }
332
333
    if( fileEditor.getPath() == null ) {
334
      getSelectionModel().select( fileEditor );
335
336
      final FileChooser fileChooser = createFileChooser( Messages.get( "Dialog.file.choose.save.title" ) );
337
      final File file = fileChooser.showSaveDialog( getWindow() );
338
      if( file == null ) {
339
        return false;
340
      }
341
342
      saveLastDirectory( file );
343
      fileEditor.setPath( file.toPath() );
344
    }
345
346
    return fileEditor.save();
347
  }
348
349
  boolean saveAllEditors() {
350
    boolean success = true;
351
352
    for( FileEditorTab fileEditor : getAllEditors() ) {
353
      if( !saveEditor( fileEditor ) ) {
354
        success = false;
355
      }
356
    }
357
358
    return success;
359
  }
360
361
  /**
362
   * Answers whether the file has had modifications. '
363
   *
364
   * @param tab THe tab to check for modifications.
365
   *
366
   * @return false The file is unmodified.
367
   */
368
  boolean canCloseEditor( final FileEditorTab tab ) {
369
    if( !tab.isModified() ) {
370
      return true;
371
    }
372
373
    final AlertMessage message = getAlertService().createAlertMessage(
374
      Messages.get( "Alert.file.close.title" ),
375
      Messages.get( "Alert.file.close.text" ),
376
      tab.getText()
377
    );
378
379
    final Alert alert = getAlertService().createAlertConfirmation( message );
380
    final ButtonType response = alert.showAndWait().get();
381
382
    return response == YES ? saveEditor( tab ) : response == NO;
383
  }
384
385
  private AlertService getAlertService() {
386
    return this.alertService;
387
  }
388
389
  boolean closeEditor( FileEditorTab fileEditor, boolean save ) {
390
    if( fileEditor == null ) {
391
      return true;
392
    }
393
394
    final Tab tab = fileEditor;
395
396
    if( save ) {
397
      Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
398
      Event.fireEvent( tab, event );
399
400
      if( event.isConsumed() ) {
401
        return false;
402
      }
403
    }
404
405
    getTabs().remove( tab );
406
407
    if( tab.getOnClosed() != null ) {
408
      Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
409
    }
410
411
    return true;
412
  }
413
414
  boolean closeAllEditors() {
415
    final FileEditorTab[] allEditors = getAllEditors();
416
    final FileEditorTab activeEditor = getActiveFileEditor();
417
418
    // try to save active tab first because in case the user decides to cancel,
419
    // then it stays active
420
    if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
421
      return false;
422
    }
423
424
    // This should be called any time a tab changes.
425
    persistPreferences();
426
427
    // save modified tabs
428
    for( int i = 0; i < allEditors.length; i++ ) {
429
      final FileEditorTab fileEditor = allEditors[ i ];
430
431
      if( fileEditor == activeEditor ) {
432
        continue;
433
      }
434
435
      if( fileEditor.isModified() ) {
436
        // activate the modified tab to make its modified content visible to the user
437
        getSelectionModel().select( i );
438
439
        if( !canCloseEditor( fileEditor ) ) {
440
          return false;
441
        }
442
      }
443
    }
444
445
    // Close all tabs.
446
    for( final FileEditorTab fileEditor : allEditors ) {
447
      if( !closeEditor( fileEditor, false ) ) {
448
        return false;
449
      }
450
    }
451
452
    return getTabs().isEmpty();
453
  }
454
455
  private FileEditorTab[] getAllEditors() {
456
    final ObservableList<Tab> tabs = getTabs();
457
    final int length = tabs.size();
458
    final FileEditorTab[] allEditors = new FileEditorTab[ length ];
459
460
    for( int i = 0; i < length; i++ ) {
461
      allEditors[ i ] = (FileEditorTab)tabs.get( i );
462
    }
463
464
    return allEditors;
465
  }
466
467
  /**
468
   * Returns the file editor tab that has the given path.
469
   *
470
   * @return null No file editor tab for the given path was found.
471
   */
472
  private FileEditorTab findEditor( final Path path ) {
473
    for( final Tab tab : getTabs() ) {
474
      final FileEditorTab fileEditor = (FileEditorTab)tab;
475
476
      if( fileEditor.isPath( path ) ) {
477
        return fileEditor;
478
      }
479
    }
480
481
    return null;
482
  }
483
484
  private FileChooser createFileChooser( String title ) {
485
    final FileChooser fileChooser = new FileChooser();
486
487
    fileChooser.setTitle( title );
488
    fileChooser.getExtensionFilters().addAll(
489
      createExtensionFilters() );
490
491
    final String lastDirectory = getState().get( "lastDirectory", null );
492
    File file = new File( (lastDirectory != null) ? lastDirectory : "." );
493
494
    if( !file.isDirectory() ) {
495
      file = new File( "." );
496
    }
497
498
    fileChooser.setInitialDirectory( file );
499
    return fileChooser;
500
  }
501
502
  private List<ExtensionFilter> createExtensionFilters() {
503
    final List<ExtensionFilter> list = new ArrayList<>();
504
505
    // TODO: Return a list of all properties that match the filter prefix.
506
    // This will allow dynamic filters to be added and removed just by
507
    // updating the properties file.
508
    list.add( createExtensionFilter( "markdown" ) );
509
    list.add( createExtensionFilter( "definition" ) );
510
    list.add( createExtensionFilter( "xml" ) );
511
    list.add( createExtensionFilter( "all" ) );
512
    return list;
513
  }
514
515
  private ExtensionFilter createExtensionFilter( final String filetype ) {
516
    final String tKey = String.format( "%s.title.%s", FILTER_EXTENSION_TITLES, filetype );
517
    final String eKey = String.format( "%s.ext.%s", FILTER_EXTENSIONS, filetype );
518
519
    return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
520
  }
521
522
  private List<String> getExtensions( final String key ) {
523
    return getSettings().getStringSettingList( key );
524
  }
525
526
  private void saveLastDirectory( final File file ) {
527
    getState().put( "lastDirectory", file.getParent() );
528
  }
529
530
  public void restorePreferences() {
531
    int activeIndex = 0;
532
533
    final Preferences preferences = getState();
534
    final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
535
    final String activeFileName = preferences.get( "activeFile", null );
536
537
    final ArrayList<File> files = new ArrayList<>( fileNames.length );
538
539
    for( final String fileName : fileNames ) {
540
      final File file = new File( fileName );
541
542
      if( file.exists() ) {
543
        files.add( file );
544
545
        if( fileName.equals( activeFileName ) ) {
546
          activeIndex = files.size() - 1;
547
        }
548
      }
549
    }
550
551
    if( files.isEmpty() ) {
552
      newEditor();
553
    } else {
554
      openEditors( files, activeIndex );
555
    }
556
  }
557
558
  public void persistPreferences() {
559
    final ObservableList<Tab> allEditors = getTabs();
560
    final List<String> fileNames = new ArrayList<>( allEditors.size() );
561
562
    for( final Tab tab : allEditors ) {
563
      final FileEditorTab fileEditor = (FileEditorTab)tab;
564
      final Path filePath = fileEditor.getPath();
565
566
      if( filePath != null ) {
567
        fileNames.add( filePath.toString() );
568
      }
569
    }
570
571
    final Preferences preferences = getState();
572
    Utils.putPrefsStrings( preferences, "file", fileNames.toArray( new String[ fileNames.size() ] ) );
573
574
    final FileEditorTab activeEditor = getActiveFileEditor();
575
    final Path filePath = activeEditor == null ? null : activeEditor.getPath();
576
577
    if( filePath == null ) {
578
      preferences.remove( "activeFile" );
579
    } else {
580
      preferences.put( "activeFile", filePath.toString() );
581
    }
582
  }
583
584
  private Settings getSettings() {
585
    return this.settings;
586
  }
587
588
  protected Options getOptions() {
589
    return this.options;
590
  }
591
592
  private Window getWindow() {
593
    return getScene().getWindow();
594
  }
595
596
  protected Preferences getState() {
30
import static com.scrivenvar.Messages.get;
31
import com.scrivenvar.predicates.files.FileTypePredicate;
32
import com.scrivenvar.service.Options;
33
import com.scrivenvar.service.Settings;
34
import com.scrivenvar.service.events.AlertMessage;
35
import com.scrivenvar.service.events.AlertService;
36
import static com.scrivenvar.service.events.AlertService.NO;
37
import static com.scrivenvar.service.events.AlertService.YES;
38
import com.scrivenvar.util.Utils;
39
import java.io.File;
40
import java.nio.file.Path;
41
import java.util.ArrayList;
42
import java.util.List;
43
import java.util.function.Consumer;
44
import java.util.prefs.Preferences;
45
import java.util.stream.Collectors;
46
import javafx.beans.property.ReadOnlyBooleanProperty;
47
import javafx.beans.property.ReadOnlyBooleanWrapper;
48
import javafx.beans.property.ReadOnlyObjectProperty;
49
import javafx.beans.property.ReadOnlyObjectWrapper;
50
import javafx.beans.value.ChangeListener;
51
import javafx.beans.value.ObservableValue;
52
import javafx.collections.ListChangeListener;
53
import javafx.collections.ObservableList;
54
import javafx.event.Event;
55
import javafx.scene.Node;
56
import javafx.scene.control.Alert;
57
import javafx.scene.control.ButtonType;
58
import javafx.scene.control.Tab;
59
import javafx.scene.control.TabPane;
60
import javafx.scene.control.TabPane.TabClosingPolicy;
61
import javafx.scene.input.InputEvent;
62
import javafx.stage.FileChooser;
63
import javafx.stage.FileChooser.ExtensionFilter;
64
import javafx.stage.Window;
65
import org.fxmisc.richtext.StyledTextArea;
66
import org.fxmisc.wellbehaved.event.EventPattern;
67
import org.fxmisc.wellbehaved.event.InputMap;
68
69
/**
70
 * Tab pane for file editors.
71
 *
72
 * @author Karl Tauber and White Magic Software, Ltd.
73
 */
74
public final class FileEditorTabPane extends TabPane {
75
76
  private final static String FILTER_EXTENSIONS = "filter.file";
77
  private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose.filter";
78
79
  private final Options options = Services.load( Options.class );
80
  private final Settings settings = Services.load( Settings.class );
81
  private final AlertService alertService = Services.load( AlertService.class );
82
83
  private final ReadOnlyObjectWrapper<Path> openDefinition = new ReadOnlyObjectWrapper<>();
84
  private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>();
85
  private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper();
86
87
  /**
88
   * Constructs a new file editor tab pane.
89
   */
90
  public FileEditorTabPane() {
91
    final ObservableList<Tab> tabs = getTabs();
92
93
    setFocusTraversable( false );
94
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
95
96
    addTabSelectionListener(
97
      (ObservableValue<? extends Tab> tabPane,
98
        final Tab oldTab, final Tab newTab) -> {
99
100
        if( newTab != null ) {
101
          activeFileEditor.set( (FileEditorTab)newTab );
102
        }
103
      }
104
    );
105
106
    final ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> {
107
      for( final Tab tab : tabs ) {
108
        if( ((FileEditorTab)tab).isModified() ) {
109
          this.anyFileEditorModified.set( true );
110
          break;
111
        }
112
      }
113
    };
114
115
    tabs.addListener(
116
      (ListChangeListener<Tab>)change -> {
117
        while( change.next() ) {
118
          if( change.wasAdded() ) {
119
            change.getAddedSubList().stream().forEach( (tab) -> {
120
              ((FileEditorTab)tab).modifiedProperty().addListener( modifiedListener );
121
            } );
122
          } else if( change.wasRemoved() ) {
123
            change.getRemoved().stream().forEach( (tab) -> {
124
              ((FileEditorTab)tab).modifiedProperty().removeListener( modifiedListener );
125
            } );
126
          }
127
        }
128
129
        // Changes in the tabs may also change anyFileEditorModified property
130
        // (e.g. closed modified file)
131
        modifiedListener.changed( null, null, null );
132
      }
133
    );
134
  }
135
136
  /**
137
   * Delegates to the active file editor.
138
   *
139
   * @param <T> Event type.
140
   * @param <U> Consumer type.
141
   * @param event Event to pass to the editor.
142
   * @param consumer Consumer to pass to the editor.
143
   */
144
  public <T extends Event, U extends T> void addEventListener(
145
    final EventPattern<? super T, ? extends U> event,
146
    final Consumer<? super U> consumer ) {
147
    getActiveFileEditor().addEventListener( event, consumer );
148
  }
149
150
  /**
151
   * Delegates to the active file editor pane, and, ultimately, to its text
152
   * area.
153
   *
154
   * @param map The map of methods to events.
155
   */
156
  public void addEventListener( final InputMap<InputEvent> map ) {
157
    getActiveFileEditor().addEventListener( map );
158
  }
159
160
  /**
161
   * Remove a keyboard event listener from the active file editor.
162
   *
163
   * @param map The keyboard events to remove.
164
   */
165
  public void removeEventListener( final InputMap<InputEvent> map ) {
166
    getActiveFileEditor().removeEventListener( map );
167
  }
168
169
  /**
170
   * Allows observers to be notified when the current file editor tab changes.
171
   *
172
   * @param listener The listener to notify of tab change events.
173
   */
174
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
175
    // Observe the tab so that when a new tab is opened or selected,
176
    // a notification is kicked off.
177
    getSelectionModel().selectedItemProperty().addListener( listener );
178
  }
179
180
  /**
181
   * Allows clients to manipulate the editor content directly.
182
   *
183
   * @return The text area for the active file editor.
184
   */
185
  public StyledTextArea getEditor() {
186
    return getActiveFileEditor().getEditorPane().getEditor();
187
  }
188
189
  public FileEditorTab getActiveFileEditor() {
190
    return this.activeFileEditor.get();
191
  }
192
193
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
194
    return this.activeFileEditor.getReadOnlyProperty();
195
  }
196
197
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
198
    return this.anyFileEditorModified.getReadOnlyProperty();
199
  }
200
201
  private FileEditorTab createFileEditor( final Path path ) {
202
    final FileEditorTab tab = new FileEditorTab( path );
203
204
    tab.setOnCloseRequest( e -> {
205
      if( !canCloseEditor( tab ) ) {
206
        e.consume();
207
      }
208
    } );
209
210
    return tab;
211
  }
212
213
  /**
214
   * Called when the user selects New from the File menu.
215
   *
216
   * @return The newly added tab.
217
   */
218
  void newEditor() {
219
    final FileEditorTab tab = createFileEditor( null );
220
221
    getTabs().add( tab );
222
    getSelectionModel().select( tab );
223
  }
224
225
  void openFileDialog() {
226
    final String title = get( "Dialog.file.choose.open.title" );
227
    final FileChooser dialog = createFileChooser( title );
228
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
229
230
    if( files != null ) {
231
      openFiles( files );
232
    }
233
  }
234
235
  /**
236
   * Opens the files into new editors, unless one of those files was a
237
   * definition file. The definition file is loaded into the definition pane,
238
   * but only the first one selected (multiple definition files will result in a
239
   * warning).
240
   *
241
   * @param files The list of non-definition files that the were requested to
242
   * open.
243
   *
244
   * @return A list of files that can be opened in text editors.
245
   */
246
  private void openFiles( final List<File> files ) {
247
    final FileTypePredicate predicate
248
      = new FileTypePredicate( createExtensionFilter( "definition" ).getExtensions() );
249
250
    // The user might have opened multiple definitions files. These will
251
    // be discarded from the text editable files.
252
    final List<File> definitions
253
      = files.stream().filter( predicate ).collect( Collectors.toList() );
254
255
    // Create a modifiable list to remove any definition files that were
256
    // opened.
257
    final List<File> editors = new ArrayList<>( files );
258
259
    if( editors.size() > 0 ) {
260
      saveLastDirectory( editors.get( 0 ) );
261
    }
262
263
    editors.removeAll( definitions );
264
265
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
266
    if( editors.size() > 0 ) {
267
      openEditors( editors, 0 );
268
    }
269
270
    if( definitions.size() > 0 ) {
271
      openDefinition( definitions.get( 0 ) );
272
    }
273
  }
274
275
  private void openEditors( final List<File> files, final int activeIndex ) {
276
    final int fileTally = files.size();
277
    final List<Tab> tabs = getTabs();
278
279
    // Close single unmodified "Untitled" tab.
280
    if( tabs.size() == 1 ) {
281
      final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 ));
282
283
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
284
        closeEditor( fileEditor, false );
285
      }
286
    }
287
288
    for( int i = 0; i < fileTally; i++ ) {
289
      final Path path = files.get( i ).toPath();
290
291
      FileEditorTab fileEditorTab = findEditor( path );
292
293
      // Only open new files.
294
      if( fileEditorTab == null ) {
295
        fileEditorTab = createFileEditor( path );
296
        getTabs().add( fileEditorTab );
297
      }
298
299
      // Select the first file in the list.
300
      if( i == activeIndex ) {
301
        getSelectionModel().select( fileEditorTab );
302
      }
303
    }
304
  }
305
306
  /**
307
   * Returns a property that changes when a new definition file is opened.
308
   *
309
   * @return The path to a definition file that was opened.
310
   */
311
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
312
    return getOnOpenDefinitionFile().getReadOnlyProperty();
313
  }
314
315
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
316
    return this.openDefinition;
317
  }
318
319
  /**
320
   * Called when the user has opened a definition file (using the file open
321
   * dialog box). This will replace the current set of definitions for the
322
   * active tab.
323
   *
324
   * @param definition The file to open.
325
   */
326
  private void openDefinition( final File definition ) {
327
    // TODO: Prevent reading this file twice when a new text document is opened.
328
    // (might be a matter of checking the value first).
329
    getOnOpenDefinitionFile().set( definition.toPath() );
330
  }
331
332
  boolean saveEditor( final FileEditorTab fileEditor ) {
333
    if( fileEditor == null || !fileEditor.isModified() ) {
334
      return true;
335
    }
336
337
    if( fileEditor.getPath() == null ) {
338
      getSelectionModel().select( fileEditor );
339
340
      final FileChooser fileChooser = createFileChooser( Messages.get( "Dialog.file.choose.save.title" ) );
341
      final File file = fileChooser.showSaveDialog( getWindow() );
342
      if( file == null ) {
343
        return false;
344
      }
345
346
      saveLastDirectory( file );
347
      fileEditor.setPath( file.toPath() );
348
    }
349
350
    return fileEditor.save();
351
  }
352
353
  boolean saveAllEditors() {
354
    boolean success = true;
355
356
    for( FileEditorTab fileEditor : getAllEditors() ) {
357
      if( !saveEditor( fileEditor ) ) {
358
        success = false;
359
      }
360
    }
361
362
    return success;
363
  }
364
365
  /**
366
   * Answers whether the file has had modifications. '
367
   *
368
   * @param tab THe tab to check for modifications.
369
   *
370
   * @return false The file is unmodified.
371
   */
372
  boolean canCloseEditor( final FileEditorTab tab ) {
373
    if( !tab.isModified() ) {
374
      return true;
375
    }
376
377
    final AlertMessage message = getAlertService().createAlertMessage(
378
      Messages.get( "Alert.file.close.title" ),
379
      Messages.get( "Alert.file.close.text" ),
380
      tab.getText()
381
    );
382
383
    final Alert alert = getAlertService().createAlertConfirmation( message );
384
    final ButtonType response = alert.showAndWait().get();
385
386
    return response == YES ? saveEditor( tab ) : response == NO;
387
  }
388
389
  private AlertService getAlertService() {
390
    return this.alertService;
391
  }
392
393
  boolean closeEditor( FileEditorTab fileEditor, boolean save ) {
394
    if( fileEditor == null ) {
395
      return true;
396
    }
397
398
    final Tab tab = fileEditor;
399
400
    if( save ) {
401
      Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
402
      Event.fireEvent( tab, event );
403
404
      if( event.isConsumed() ) {
405
        return false;
406
      }
407
    }
408
409
    getTabs().remove( tab );
410
411
    if( tab.getOnClosed() != null ) {
412
      Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
413
    }
414
415
    return true;
416
  }
417
418
  boolean closeAllEditors() {
419
    final FileEditorTab[] allEditors = getAllEditors();
420
    final FileEditorTab activeEditor = getActiveFileEditor();
421
422
    // try to save active tab first because in case the user decides to cancel,
423
    // then it stays active
424
    if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
425
      return false;
426
    }
427
428
    // This should be called any time a tab changes.
429
    persistPreferences();
430
431
    // save modified tabs
432
    for( int i = 0; i < allEditors.length; i++ ) {
433
      final FileEditorTab fileEditor = allEditors[ i ];
434
435
      if( fileEditor == activeEditor ) {
436
        continue;
437
      }
438
439
      if( fileEditor.isModified() ) {
440
        // activate the modified tab to make its modified content visible to the user
441
        getSelectionModel().select( i );
442
443
        if( !canCloseEditor( fileEditor ) ) {
444
          return false;
445
        }
446
      }
447
    }
448
449
    // Close all tabs.
450
    for( final FileEditorTab fileEditor : allEditors ) {
451
      if( !closeEditor( fileEditor, false ) ) {
452
        return false;
453
      }
454
    }
455
456
    return getTabs().isEmpty();
457
  }
458
459
  private FileEditorTab[] getAllEditors() {
460
    final ObservableList<Tab> tabs = getTabs();
461
    final int length = tabs.size();
462
    final FileEditorTab[] allEditors = new FileEditorTab[ length ];
463
464
    for( int i = 0; i < length; i++ ) {
465
      allEditors[ i ] = (FileEditorTab)tabs.get( i );
466
    }
467
468
    return allEditors;
469
  }
470
471
  /**
472
   * Returns the file editor tab that has the given path.
473
   *
474
   * @return null No file editor tab for the given path was found.
475
   */
476
  private FileEditorTab findEditor( final Path path ) {
477
    for( final Tab tab : getTabs() ) {
478
      final FileEditorTab fileEditor = (FileEditorTab)tab;
479
480
      if( fileEditor.isPath( path ) ) {
481
        return fileEditor;
482
      }
483
    }
484
485
    return null;
486
  }
487
488
  private FileChooser createFileChooser( String title ) {
489
    final FileChooser fileChooser = new FileChooser();
490
491
    fileChooser.setTitle( title );
492
    fileChooser.getExtensionFilters().addAll(
493
      createExtensionFilters() );
494
495
    final String lastDirectory = getPreferences().get( "lastDirectory", null );
496
    File file = new File( (lastDirectory != null) ? lastDirectory : "." );
497
498
    if( !file.isDirectory() ) {
499
      file = new File( "." );
500
    }
501
502
    fileChooser.setInitialDirectory( file );
503
    return fileChooser;
504
  }
505
506
  private List<ExtensionFilter> createExtensionFilters() {
507
    final List<ExtensionFilter> list = new ArrayList<>();
508
509
    // TODO: Return a list of all properties that match the filter prefix.
510
    // This will allow dynamic filters to be added and removed just by
511
    // updating the properties file.
512
    list.add( createExtensionFilter( "definition" ) );
513
    list.add( createExtensionFilter( "markdown" ) );
514
    list.add( createExtensionFilter( "xml" ) );
515
    list.add( createExtensionFilter( "all" ) );
516
    return list;
517
  }
518
519
  private ExtensionFilter createExtensionFilter( final String filetype ) {
520
    final String tKey = String.format( "%s.title.%s", FILTER_EXTENSION_TITLES, filetype );
521
    final String eKey = String.format( "%s.ext.%s", FILTER_EXTENSIONS, filetype );
522
523
    return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
524
  }
525
526
  private List<String> getExtensions( final String key ) {
527
    return getSettings().getStringSettingList( key );
528
  }
529
530
  private void saveLastDirectory( final File file ) {
531
    getPreferences().put( "lastDirectory", file.getParent() );
532
  }
533
534
  public void restorePreferences() {
535
    int activeIndex = 0;
536
537
    final Preferences preferences = getPreferences();
538
    final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
539
    final String activeFileName = preferences.get( "activeFile", null );
540
541
    final ArrayList<File> files = new ArrayList<>( fileNames.length );
542
543
    for( final String fileName : fileNames ) {
544
      final File file = new File( fileName );
545
546
      if( file.exists() ) {
547
        files.add( file );
548
549
        if( fileName.equals( activeFileName ) ) {
550
          activeIndex = files.size() - 1;
551
        }
552
      }
553
    }
554
555
    if( files.isEmpty() ) {
556
      newEditor();
557
    } else {
558
      openEditors( files, activeIndex );
559
    }
560
  }
561
562
  public void persistPreferences() {
563
    final ObservableList<Tab> allEditors = getTabs();
564
    final List<String> fileNames = new ArrayList<>( allEditors.size() );
565
566
    for( final Tab tab : allEditors ) {
567
      final FileEditorTab fileEditor = (FileEditorTab)tab;
568
      final Path filePath = fileEditor.getPath();
569
570
      if( filePath != null ) {
571
        fileNames.add( filePath.toString() );
572
      }
573
    }
574
575
    final Preferences preferences = getPreferences();
576
    Utils.putPrefsStrings( preferences, "file", fileNames.toArray( new String[ fileNames.size() ] ) );
577
578
    final FileEditorTab activeEditor = getActiveFileEditor();
579
    final Path filePath = activeEditor == null ? null : activeEditor.getPath();
580
581
    if( filePath == null ) {
582
      preferences.remove( "activeFile" );
583
    } else {
584
      preferences.put( "activeFile", filePath.toString() );
585
    }
586
  }
587
588
  private Settings getSettings() {
589
    return this.settings;
590
  }
591
592
  protected Options getOptions() {
593
    return this.options;
594
  }
595
596
  private Window getWindow() {
597
    return getScene().getWindow();
598
  }
599
600
  private Preferences getPreferences() {
597601
    return getOptions().getState();
598602
  }
M src/main/java/com/scrivenvar/MainWindow.java
2929
3030
import static com.scrivenvar.Constants.FILE_LOGO_32;
31
import static com.scrivenvar.Constants.STYLESHEET_SCENE;
32
import static com.scrivenvar.Messages.get;
33
import com.scrivenvar.definition.DefinitionFactory;
34
import com.scrivenvar.definition.DefinitionPane;
35
import com.scrivenvar.definition.DefinitionSource;
36
import com.scrivenvar.editors.VariableNameInjector;
37
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
38
import com.scrivenvar.preview.HTMLPreviewPane;
39
import com.scrivenvar.processors.HTMLPreviewProcessor;
40
import com.scrivenvar.processors.MarkdownCaretInsertionProcessor;
41
import com.scrivenvar.processors.MarkdownCaretReplacementProcessor;
42
import com.scrivenvar.processors.MarkdownProcessor;
43
import com.scrivenvar.processors.Processor;
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 static com.scrivenvar.util.StageState.K_PANE_SPLIT_PREVIEW;
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.File;
68
import java.nio.file.Path;
69
import java.util.HashMap;
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
103
/**
104
 * Main window containing a tab pane in the center for file editors.
105
 *
106
 * @author Karl Tauber and White Magic Software, Ltd.
107
 */
108
public class MainWindow {
109
110
  private final Options options = Services.load( Options.class );
111
112
  private Scene scene;
113
114
  private DefinitionPane definitionPane;
115
  private FileEditorTabPane fileEditorPane;
116
  private HTMLPreviewPane previewPane;
117
118
  private VariableNameInjector variableNameInjector;
119
120
  private MenuBar menuBar;
121
122
  public MainWindow() {
123
    initLayout();
124
    initOpenDefinitionListener();
125
    initTabAddedListener();
126
    initTabChangeListener();
127
    initPreferences();
128
    initVariableNameInjector();
129
  }
130
131
  /**
132
   * Listen for file editor tab pane to receive an open definition source event.
133
   */
134
  private void initOpenDefinitionListener() {
135
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
136
      (ObservableValue<? extends Path> definitionFile,
137
        final Path oldPath, final Path newPath) -> {
138
        final DefinitionSource ds = createDefinitionSource( newPath );
139
        associate( ds, getActiveFileEditor() );
140
      } );
141
  }
142
143
  /**
144
   * When tabs are added, hook the various change listeners onto the new tab so
145
   * that the preview pane refreshes as necessary.
146
   */
147
  private void initTabAddedListener() {
148
    final FileEditorTabPane editorPane = getFileEditorPane();
149
150
    // Make sure the text processor kicks off when new files are opened.
151
    final ObservableList<Tab> tabs = editorPane.getTabs();
152
153
    // Update the preview pane on tab changes.
154
    tabs.addListener(
155
      (final Change<? extends Tab> change) -> {
156
        while( change.next() ) {
157
          if( change.wasAdded() ) {
158
            // Multiple tabs can be added simultaneously.
159
            for( final Tab newTab : change.getAddedSubList() ) {
160
              final FileEditorTab tab = (FileEditorTab)newTab;
161
162
              initTextChangeListener( tab );
163
              initCaretParagraphListener( tab );
164
            }
165
          }
166
        }
167
      }
168
    );
169
  }
170
171
  /**
172
   * Reloads the preferences from the previous load.
173
   */
174
  private void initPreferences() {
175
    getFileEditorPane().restorePreferences();
176
  }
177
178
  /**
179
   * Listen for new tab selection events.
180
   */
181
  private void initTabChangeListener() {
182
    final FileEditorTabPane editorPane = getFileEditorPane();
183
184
    // Update the preview pane changing tabs.
185
    editorPane.addTabSelectionListener(
186
      (ObservableValue<? extends Tab> tabPane,
187
        final Tab oldTab, final Tab newTab) -> {
188
189
        // If there was no old tab, then this is a first time load, which
190
        // can be ignored.
191
        if( oldTab != null ) {
192
          if( newTab == null ) {
193
            closeRemainingTab();
194
          } else {
195
            // Synchronize the preview with the edited text.
196
            refreshSelectedTab( (FileEditorTab)newTab );
197
          }
198
        }
199
      }
200
    );
201
  }
202
203
  /**
204
   * Initialize the variable name editor.
205
   */
206
  private void initVariableNameInjector() {
207
    setVariableNameInjector(
208
      new VariableNameInjector( getFileEditorPane(), getDefinitionPane() )
209
    );
210
  }
211
212
  private void initTextChangeListener( final FileEditorTab tab ) {
213
    tab.addTextChangeListener(
214
      (ObservableValue<? extends String> editor,
215
        final String oldValue, final String newValue) -> {
216
        refreshSelectedTab( tab );
217
      }
218
    );
219
  }
220
221
  private void initCaretParagraphListener( final FileEditorTab tab ) {
222
    tab.addCaretParagraphListener(
223
      (ObservableValue<? extends Integer> editor,
224
        final Integer oldValue, final Integer newValue) -> {
225
        refreshSelectedTab( tab );
226
      }
227
    );
228
  }
229
230
  /**
231
   * Called whenever the preview pane becomes out of sync with the file editor
232
   * tab. This can be called when the text changes, the caret paragraph changes,
233
   * or the file tab changes.
234
   *
235
   * @param tab The file editor tab that has been changed in some fashion.
236
   */
237
  private void refreshSelectedTab( final FileEditorTab tab ) {
238
    final HTMLPreviewPane preview = getPreviewPane();
239
    preview.setPath( tab.getPath() );
240
241
    final Processor<String> hpp = new HTMLPreviewProcessor( preview );
242
    final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp );
243
    final Processor<String> mp = new MarkdownProcessor( mcrp );
244
    final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, tab.getCaretPosition() );
245
    final Processor<String> vp = new VariableProcessor( mcip, getResolvedMap() );
246
247
    vp.processChain( tab.getEditorText() );
248
  }
249
250
  /**
251
   * TODO: Patch into loading of definition source.
252
   *
253
   * @return
254
   */
255
  private Map<String, String> getResolvedMap() {
256
    return new HashMap<>();
257
  }
258
259
  /**
260
   * TODO: Patch into loading of definition source.
261
   *
262
   * @return
263
   */
264
  private TreeView<String> getTreeView() {
265
    return new TreeView<>();
266
  }
267
268
  /**
269
   * Called when the tab has changed to a new editor to replace the current
270
   * definition pane with the
271
   *
272
   * @param tab Reference to the tab that has the file being edited.
273
   */
274
  private void updateDefinitionPane( final FileEditorTab tab ) {
275
    // Look up the path to the variable definition file associated with the
276
    // given tab.
277
    final Path path = getVariableDefinitionPath( tab.getPath() );
278
    final DefinitionSource ds = createDefinitionSource( path );
279
280
    associate( ds, tab );
281
  }
282
283
  private void associate( final DefinitionSource ds, final FileEditorTab tab ) {
284
    System.out.println( "Associate " + ds + " with " + tab );
285
  }
286
287
  /**
288
   * Searches the persistent settings for the variable definition file that is
289
   * associated with the given path.
290
   *
291
   * @param tabPath The path that may be associated with some variables.
292
   *
293
   * @return A path to the variable definition file for the given document path.
294
   */
295
  private Path getVariableDefinitionPath( final Path tabPath ) {
296
    return new File( "/tmp/variables.yaml" ).toPath();
297
  }
298
299
  /**
300
   * Creates a boolean property that is bound to another boolean value of the
301
   * active editor.
302
   */
303
  private BooleanProperty createActiveBooleanProperty(
304
    final Function<FileEditorTab, ObservableBooleanValue> func ) {
305
306
    final BooleanProperty b = new SimpleBooleanProperty();
307
    final FileEditorTab tab = getActiveFileEditor();
308
309
    if( tab != null ) {
310
      b.bind( func.apply( tab ) );
311
    }
312
313
    getFileEditorPane().activeFileEditorProperty().addListener(
314
      (observable, oldFileEditor, newFileEditor) -> {
315
        b.unbind();
316
317
        if( newFileEditor != null ) {
318
          b.bind( func.apply( newFileEditor ) );
319
        } else {
320
          b.set( false );
321
        }
322
      }
323
    );
324
325
    return b;
326
  }
327
328
  /**
329
   * Called when the last open tab is closed. This clears out the preview pane
330
   * and the definition pane.
331
   */
332
  private void closeRemainingTab() {
333
    getPreviewPane().clear();
334
    getDefinitionPane().clear();
335
  }
336
337
  //---- File actions -------------------------------------------------------
338
  private void fileNew() {
339
    getFileEditorPane().newEditor();
340
  }
341
342
  private void fileOpen() {
343
    getFileEditorPane().openFileDialog();
344
  }
345
346
  private void fileClose() {
347
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
348
  }
349
350
  private void fileCloseAll() {
351
    getFileEditorPane().closeAllEditors();
352
  }
353
354
  private void fileSave() {
355
    getFileEditorPane().saveEditor( getActiveFileEditor() );
356
  }
357
358
  private void fileSaveAll() {
359
    getFileEditorPane().saveAllEditors();
360
  }
361
362
  private void fileExit() {
363
    final Window window = getWindow();
364
    Event.fireEvent( window,
365
      new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) );
366
  }
367
368
  //---- Help actions -------------------------------------------------------
369
  private void helpAbout() {
370
    Alert alert = new Alert( AlertType.INFORMATION );
371
    alert.setTitle( get( "Dialog.about.title" ) );
372
    alert.setHeaderText( get( "Dialog.about.header" ) );
373
    alert.setContentText( get( "Dialog.about.content" ) );
374
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
375
    alert.initOwner( getWindow() );
376
377
    alert.showAndWait();
378
  }
379
380
  //---- Convenience accessors ----------------------------------------------
381
  private float getFloat( final String key, final float defaultValue ) {
382
    return getPreferences().getFloat( key, defaultValue );
383
  }
384
385
  private Preferences getPreferences() {
386
    return getOptions().getState();
387
  }
388
389
  private Window getWindow() {
390
    return getScene().getWindow();
391
  }
392
393
  private MarkdownEditorPane getActiveEditor() {
394
    return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane());
395
  }
396
397
  private FileEditorTab getActiveFileEditor() {
398
    return getFileEditorPane().getActiveFileEditor();
399
  }
400
401
  //---- Member accessors ---------------------------------------------------
402
  public Scene getScene() {
403
    return this.scene;
404
  }
405
406
  private void setScene( Scene scene ) {
407
    this.scene = scene;
408
  }
409
410
  private FileEditorTabPane getFileEditorPane() {
411
    if( this.fileEditorPane == null ) {
412
      this.fileEditorPane = createFileEditorPane();
413
    }
414
415
    return this.fileEditorPane;
416
  }
417
418
  private synchronized HTMLPreviewPane getPreviewPane() {
419
    if( this.previewPane == null ) {
420
      this.previewPane = createPreviewPane();
421
    }
422
423
    return this.previewPane;
424
  }
425
426
  private DefinitionPane getDefinitionPane() {
427
    if( this.definitionPane == null ) {
428
      this.definitionPane = createDefinitionPane();
429
    }
430
431
    return this.definitionPane;
432
  }
433
434
  public VariableNameInjector getVariableNameInjector() {
435
    return this.variableNameInjector;
436
  }
437
438
  public void setVariableNameInjector( final VariableNameInjector injector ) {
439
    this.variableNameInjector = injector;
440
  }
441
442
  private Options getOptions() {
443
    return this.options;
444
  }
445
446
  public MenuBar getMenuBar() {
447
    return this.menuBar;
448
  }
449
450
  public void setMenuBar( MenuBar menuBar ) {
451
    this.menuBar = menuBar;
452
  }
453
454
  //---- Member creators ----------------------------------------------------
455
  private DefinitionSource createDefinitionSource( final Path path ) {
456
    return createDefinitionFactory().fileDefinitionSource( path );
457
  }
458
459
  /**
460
   * Create an editor pane to hold file editor tabs.
461
   *
462
   * @return A new instance, never null.
463
   */
464
  private FileEditorTabPane createFileEditorPane() {
465
    return new FileEditorTabPane();
466
  }
467
468
  private HTMLPreviewPane createPreviewPane() {
469
    return new HTMLPreviewPane();
470
  }
471
472
  protected DefinitionPane createDefinitionPane() {
473
    return new DefinitionPane( getTreeView() );
474
  }
475
476
  private DefinitionFactory createDefinitionFactory() {
477
    return new DefinitionFactory();
478
  }
479
480
  private Node createMenuBar() {
481
    final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
482
483
    // File actions
484
    Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
485
    Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
486
    Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
487
    Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
488
    Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
489
      createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
490
    Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
491
      Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
492
    Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
493
494
    // Edit actions
495
    Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
496
      e -> getActiveEditor().undo(),
497
      createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
498
    Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
499
      e -> getActiveEditor().redo(),
500
      createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
501
502
    // Insert actions
503
    Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
504
      e -> getActiveEditor().surroundSelection( "**", "**" ),
505
      activeFileEditorIsNull );
506
    Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
507
      e -> getActiveEditor().surroundSelection( "*", "*" ),
508
      activeFileEditorIsNull );
509
    Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
510
      e -> getActiveEditor().surroundSelection( "~~", "~~" ),
511
      activeFileEditorIsNull );
512
    Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
513
      e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
514
      activeFileEditorIsNull );
515
    Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
516
      e -> getActiveEditor().surroundSelection( "`", "`" ),
517
      activeFileEditorIsNull );
518
    Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
519
      e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ),
520
      activeFileEditorIsNull );
521
522
    Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
523
      e -> getActiveEditor().insertLink(),
524
      activeFileEditorIsNull );
525
    Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT,
526
      e -> getActiveEditor().insertImage(),
527
      activeFileEditorIsNull );
528
529
    final Action[] headers = new Action[ 6 ];
530
531
    // Insert header actions (H1 ... H6)
532
    for( int i = 1; i <= 6; i++ ) {
533
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
534
      final String markup = String.format( "\n\n%s ", hashes );
535
      final String text = get( "Main.menu.insert.header_" + i );
536
      final String accelerator = "Shortcut+" + i;
537
      final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
538
539
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
540
        e -> getActiveEditor().surroundSelection( markup, "", prompt ),
541
        activeFileEditorIsNull );
542
    }
543
544
    Action insertUnorderedListAction = new Action( get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
545
      e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
546
      activeFileEditorIsNull );
547
    Action insertOrderedListAction = new Action( get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
548
      e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
549
      activeFileEditorIsNull );
550
    Action insertHorizontalRuleAction = new Action( get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
551
      e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
552
      activeFileEditorIsNull );
553
554
    // Help actions
555
    Action helpAboutAction = new Action( get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
556
557
    //---- MenuBar ----
558
    Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ),
559
      fileNewAction,
560
      fileOpenAction,
561
      null,
562
      fileCloseAction,
563
      fileCloseAllAction,
564
      null,
565
      fileSaveAction,
566
      fileSaveAllAction,
567
      null,
568
      fileExitAction );
569
570
    Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ),
571
      editUndoAction,
572
      editRedoAction );
573
574
    Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ),
575
      insertBoldAction,
576
      insertItalicAction,
577
      insertStrikethroughAction,
578
      insertBlockquoteAction,
579
      insertCodeAction,
580
      insertFencedCodeBlockAction,
581
      null,
582
      insertLinkAction,
583
      insertImageAction,
584
      null,
585
      headers[ 0 ],
586
      headers[ 1 ],
587
      headers[ 2 ],
588
      headers[ 3 ],
589
      headers[ 4 ],
590
      headers[ 5 ],
591
      null,
592
      insertUnorderedListAction,
593
      insertOrderedListAction,
594
      insertHorizontalRuleAction );
595
596
    Menu helpMenu = ActionUtils.createMenu( get( "Main.menu.help" ),
597
      helpAboutAction );
598
599
    menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu );
600
601
    //---- ToolBar ----
602
    ToolBar toolBar = ActionUtils.createToolBar(
603
      fileNewAction,
604
      fileOpenAction,
605
      fileSaveAction,
606
      null,
607
      editUndoAction,
608
      editRedoAction,
609
      null,
610
      insertBoldAction,
611
      insertItalicAction,
612
      insertBlockquoteAction,
613
      insertCodeAction,
614
      insertFencedCodeBlockAction,
615
      null,
616
      insertLinkAction,
617
      insertImageAction,
618
      null,
619
      headers[ 0 ],
620
      null,
621
      insertUnorderedListAction,
622
      insertOrderedListAction );
623
624
    return new VBox( menuBar, toolBar );
31
import static com.scrivenvar.Constants.PREFS_DEFINITION_SOURCE;
32
import static com.scrivenvar.Constants.STYLESHEET_SCENE;
33
import static com.scrivenvar.Messages.get;
34
import com.scrivenvar.definition.DefinitionFactory;
35
import com.scrivenvar.definition.DefinitionPane;
36
import com.scrivenvar.definition.DefinitionSource;
37
import com.scrivenvar.definition.EmptyDefinitionSource;
38
import com.scrivenvar.editors.VariableNameInjector;
39
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
40
import com.scrivenvar.preview.HTMLPreviewPane;
41
import com.scrivenvar.processors.HTMLPreviewProcessor;
42
import com.scrivenvar.processors.MarkdownCaretInsertionProcessor;
43
import com.scrivenvar.processors.MarkdownCaretReplacementProcessor;
44
import com.scrivenvar.processors.MarkdownProcessor;
45
import com.scrivenvar.processors.Processor;
46
import com.scrivenvar.processors.VariableProcessor;
47
import com.scrivenvar.service.Options;
48
import com.scrivenvar.util.Action;
49
import com.scrivenvar.util.ActionUtils;
50
import static com.scrivenvar.util.StageState.K_PANE_SPLIT_DEFINITION;
51
import static com.scrivenvar.util.StageState.K_PANE_SPLIT_EDITOR;
52
import static com.scrivenvar.util.StageState.K_PANE_SPLIT_PREVIEW;
53
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD;
54
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE;
55
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT;
56
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT;
57
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT;
58
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT;
59
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER;
60
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC;
61
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK;
62
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL;
63
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL;
64
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT;
65
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT;
66
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT;
67
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH;
68
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO;
69
import java.net.MalformedURLException;
70
import java.nio.file.Path;
71
import java.util.Map;
72
import java.util.function.Function;
73
import java.util.prefs.Preferences;
74
import javafx.beans.binding.Bindings;
75
import javafx.beans.binding.BooleanBinding;
76
import javafx.beans.property.BooleanProperty;
77
import javafx.beans.property.SimpleBooleanProperty;
78
import javafx.beans.value.ObservableBooleanValue;
79
import javafx.beans.value.ObservableValue;
80
import javafx.collections.ListChangeListener.Change;
81
import javafx.collections.ObservableList;
82
import javafx.event.Event;
83
import javafx.scene.Node;
84
import javafx.scene.Scene;
85
import javafx.scene.control.Alert;
86
import javafx.scene.control.Alert.AlertType;
87
import javafx.scene.control.Menu;
88
import javafx.scene.control.MenuBar;
89
import javafx.scene.control.SplitPane;
90
import javafx.scene.control.Tab;
91
import javafx.scene.control.ToolBar;
92
import javafx.scene.control.TreeView;
93
import javafx.scene.image.Image;
94
import javafx.scene.image.ImageView;
95
import static javafx.scene.input.KeyCode.ESCAPE;
96
import javafx.scene.input.KeyEvent;
97
import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
98
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
99
import javafx.scene.layout.BorderPane;
100
import javafx.scene.layout.VBox;
101
import javafx.stage.Window;
102
import javafx.stage.WindowEvent;
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
  private MenuBar menuBar;
115
116
  private DefinitionPane definitionPane;
117
  private FileEditorTabPane fileEditorPane;
118
  private HTMLPreviewPane previewPane;
119
120
  private VariableNameInjector variableNameInjector;
121
  private DefinitionSource definitionSource;
122
123
  public MainWindow() {
124
    initLayout();
125
    initOpenDefinitionListener();
126
    initTabAddedListener();
127
    initTabChangedListener();
128
    initPreferences();
129
    initVariableNameInjector();
130
  }
131
132
  /**
133
   * Listen for file editor tab pane to receive an open definition source event.
134
   */
135
  private void initOpenDefinitionListener() {
136
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
137
      (ObservableValue<? extends Path> definitionFile,
138
        final Path oldPath, final Path newPath) -> {
139
        openDefinition( newPath );
140
      } );
141
  }
142
143
  /**
144
   * When tabs are added, hook the various change listeners onto the new tab so
145
   * that the preview pane refreshes as necessary.
146
   */
147
  private void initTabAddedListener() {
148
    final FileEditorTabPane editorPane = getFileEditorPane();
149
150
    // Make sure the text processor kicks off when new files are opened.
151
    final ObservableList<Tab> tabs = editorPane.getTabs();
152
153
    // Update the preview pane on tab changes.
154
    tabs.addListener(
155
      (final Change<? extends Tab> change) -> {
156
        while( change.next() ) {
157
          if( change.wasAdded() ) {
158
            // Multiple tabs can be added simultaneously.
159
            for( final Tab newTab : change.getAddedSubList() ) {
160
              final FileEditorTab tab = (FileEditorTab)newTab;
161
162
              initTextChangeListener( tab );
163
              initCaretParagraphListener( tab );
164
            }
165
          }
166
        }
167
      }
168
    );
169
  }
170
171
  /**
172
   * Reloads the preferences from the previous load.
173
   */
174
  private void initPreferences() {
175
    getFileEditorPane().restorePreferences();
176
    restoreDefinitionSource();
177
  }
178
179
  /**
180
   * Listen for new tab selection events.
181
   */
182
  private void initTabChangedListener() {
183
    final FileEditorTabPane editorPane = getFileEditorPane();
184
185
    // Update the preview pane changing tabs.
186
    editorPane.addTabSelectionListener(
187
      (ObservableValue<? extends Tab> tabPane,
188
        final Tab oldTab, final Tab newTab) -> {
189
190
        // If there was no old tab, then this is a first time load, which
191
        // can be ignored.
192
        if( oldTab != null ) {
193
          if( newTab == null ) {
194
            closeRemainingTab();
195
          } else {
196
            // Synchronize the preview with the edited text.
197
            refreshSelectedTab( (FileEditorTab)newTab );
198
          }
199
        }
200
      }
201
    );
202
  }
203
204
  /**
205
   * Initialize the variable name editor.
206
   */
207
  private void initVariableNameInjector() {
208
    setVariableNameInjector(
209
      new VariableNameInjector( getFileEditorPane(), getDefinitionPane() )
210
    );
211
  }
212
213
  private void initTextChangeListener( final FileEditorTab tab ) {
214
    tab.addTextChangeListener(
215
      (ObservableValue<? extends String> editor,
216
        final String oldValue, final String newValue) -> {
217
        refreshSelectedTab( tab );
218
      }
219
    );
220
  }
221
222
  private void initCaretParagraphListener( final FileEditorTab tab ) {
223
    tab.addCaretParagraphListener(
224
      (ObservableValue<? extends Integer> editor,
225
        final Integer oldValue, final Integer newValue) -> {
226
        refreshSelectedTab( tab );
227
      }
228
    );
229
  }
230
231
  /**
232
   * Called whenever the preview pane becomes out of sync with the file editor
233
   * tab. This can be called when the text changes, the caret paragraph changes,
234
   * or the file tab changes.
235
   *
236
   * @param tab The file editor tab that has been changed in some fashion.
237
   */
238
  private void refreshSelectedTab( final FileEditorTab tab ) {
239
    final HTMLPreviewPane preview = getPreviewPane();
240
    preview.setPath( tab.getPath() );
241
242
    final Processor<String> hpp = new HTMLPreviewProcessor( preview );
243
    final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp );
244
    final Processor<String> mp = new MarkdownProcessor( mcrp );
245
    final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, tab.getCaretPosition() );
246
    final Processor<String> vp = new VariableProcessor( mcip, getResolvedMap() );
247
248
    vp.processChain( tab.getEditorText() );
249
  }
250
251
  /**
252
   * Returns the variable map of interpolated definitions.
253
   *
254
   * @return A map to help dereference variables.
255
   */
256
  private Map<String, String> getResolvedMap() {
257
    return getDefinitionSource().getResolvedMap();
258
  }
259
260
  /**
261
   * Returns the root node for the hierarchical definition source.
262
   *
263
   * @return Data to display in the definition pane.
264
   */
265
  private TreeView<String> getTreeView() {
266
    try {
267
      return getDefinitionSource().asTreeView();
268
    } catch( Exception e ) {
269
      alert( e );
270
    }
271
272
    return new TreeView<>();
273
  }
274
275
  private void openDefinition( final Path path ) {
276
    openDefinition( path.toString() );
277
  }
278
279
  private void openDefinition( final String path ) {
280
    try {
281
      final DefinitionSource ds = createDefinitionSource( path );
282
      setDefinitionSource( ds );
283
      storeDefinitionSource();
284
285
      getDefinitionPane().setRoot( ds.asTreeView() );
286
    } catch( Exception e ) {
287
      alert( e );
288
    }
289
  }
290
291
  private void restoreDefinitionSource() {
292
    final Preferences preferences = getPreferences();
293
    final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
294
295
    if( source != null ) {
296
      openDefinition( source );
297
    }
298
  }
299
300
  private void storeDefinitionSource() {
301
    final Preferences preferences = getPreferences();
302
    final DefinitionSource ds = getDefinitionSource();
303
304
    preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
305
  }
306
307
  /**
308
   * Called when the last open tab is closed. This clears out the preview pane
309
   * and the definition pane.
310
   */
311
  private void closeRemainingTab() {
312
    getPreviewPane().clear();
313
    getDefinitionPane().clear();
314
  }
315
316
  /**
317
   * Called when an exception occurs that warrants the user's attention.
318
   *
319
   * @param e The exception with a message that the user should know about.
320
   */
321
  private void alert( final Exception e ) {
322
    // TODO: Raise a notice.
323
  }
324
325
  //---- File actions -------------------------------------------------------
326
  private void fileNew() {
327
    getFileEditorPane().newEditor();
328
  }
329
330
  private void fileOpen() {
331
    getFileEditorPane().openFileDialog();
332
  }
333
334
  private void fileClose() {
335
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
336
  }
337
338
  private void fileCloseAll() {
339
    getFileEditorPane().closeAllEditors();
340
  }
341
342
  private void fileSave() {
343
    getFileEditorPane().saveEditor( getActiveFileEditor() );
344
  }
345
346
  private void fileSaveAll() {
347
    getFileEditorPane().saveAllEditors();
348
  }
349
350
  private void fileExit() {
351
    final Window window = getWindow();
352
    Event.fireEvent( window,
353
      new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) );
354
  }
355
356
  //---- Help actions -------------------------------------------------------
357
  private void helpAbout() {
358
    Alert alert = new Alert( AlertType.INFORMATION );
359
    alert.setTitle( get( "Dialog.about.title" ) );
360
    alert.setHeaderText( get( "Dialog.about.header" ) );
361
    alert.setContentText( get( "Dialog.about.content" ) );
362
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
363
    alert.initOwner( getWindow() );
364
365
    alert.showAndWait();
366
  }
367
368
  //---- Convenience accessors ----------------------------------------------
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 Window getWindow() {
378
    return getScene().getWindow();
379
  }
380
381
  private MarkdownEditorPane getActiveEditor() {
382
    return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane());
383
  }
384
385
  private FileEditorTab getActiveFileEditor() {
386
    return getFileEditorPane().getActiveFileEditor();
387
  }
388
389
  //---- Member accessors ---------------------------------------------------
390
  public Scene getScene() {
391
    return this.scene;
392
  }
393
394
  private void setScene( Scene scene ) {
395
    this.scene = scene;
396
  }
397
398
  private FileEditorTabPane getFileEditorPane() {
399
    if( this.fileEditorPane == null ) {
400
      this.fileEditorPane = createFileEditorPane();
401
    }
402
403
    return this.fileEditorPane;
404
  }
405
406
  private synchronized HTMLPreviewPane getPreviewPane() {
407
    if( this.previewPane == null ) {
408
      this.previewPane = createPreviewPane();
409
    }
410
411
    return this.previewPane;
412
  }
413
414
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
415
    this.definitionSource = definitionSource;
416
  }
417
418
  private synchronized DefinitionSource getDefinitionSource() {
419
    if( this.definitionSource == null ) {
420
      this.definitionSource = new EmptyDefinitionSource();
421
    }
422
423
    return this.definitionSource;
424
  }
425
426
  private DefinitionPane getDefinitionPane() {
427
    if( this.definitionPane == null ) {
428
      this.definitionPane = createDefinitionPane();
429
    }
430
431
    return this.definitionPane;
432
  }
433
434
  public VariableNameInjector getVariableNameInjector() {
435
    return this.variableNameInjector;
436
  }
437
438
  public void setVariableNameInjector( final VariableNameInjector injector ) {
439
    this.variableNameInjector = injector;
440
  }
441
442
  private Options getOptions() {
443
    return this.options;
444
  }
445
446
  public MenuBar getMenuBar() {
447
    return this.menuBar;
448
  }
449
450
  public void setMenuBar( MenuBar menuBar ) {
451
    this.menuBar = menuBar;
452
  }
453
454
  //---- Member creators ----------------------------------------------------
455
  private DefinitionSource createDefinitionSource( final String path )
456
    throws MalformedURLException {
457
    return createDefinitionFactory().createDefinitionSource( path );
458
  }
459
460
  /**
461
   * Create an editor pane to hold file editor tabs.
462
   *
463
   * @return A new instance, never null.
464
   */
465
  private FileEditorTabPane createFileEditorPane() {
466
    return new FileEditorTabPane();
467
  }
468
469
  private HTMLPreviewPane createPreviewPane() {
470
    return new HTMLPreviewPane();
471
  }
472
473
  private DefinitionPane createDefinitionPane() {
474
    return new DefinitionPane( getTreeView() );
475
  }
476
477
  private DefinitionFactory createDefinitionFactory() {
478
    return new DefinitionFactory();
479
  }
480
481
  private Node createMenuBar() {
482
    final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
483
484
    // File actions
485
    Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
486
    Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
487
    Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
488
    Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
489
    Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
490
      createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
491
    Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
492
      Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
493
    Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
494
495
    // Edit actions
496
    Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
497
      e -> getActiveEditor().undo(),
498
      createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
499
    Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
500
      e -> getActiveEditor().redo(),
501
      createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
502
503
    // Insert actions
504
    Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
505
      e -> getActiveEditor().surroundSelection( "**", "**" ),
506
      activeFileEditorIsNull );
507
    Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
508
      e -> getActiveEditor().surroundSelection( "*", "*" ),
509
      activeFileEditorIsNull );
510
    Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
511
      e -> getActiveEditor().surroundSelection( "~~", "~~" ),
512
      activeFileEditorIsNull );
513
    Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
514
      e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
515
      activeFileEditorIsNull );
516
    Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
517
      e -> getActiveEditor().surroundSelection( "`", "`" ),
518
      activeFileEditorIsNull );
519
    Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
520
      e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ),
521
      activeFileEditorIsNull );
522
523
    Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
524
      e -> getActiveEditor().insertLink(),
525
      activeFileEditorIsNull );
526
    Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT,
527
      e -> getActiveEditor().insertImage(),
528
      activeFileEditorIsNull );
529
530
    final Action[] headers = new Action[ 6 ];
531
532
    // Insert header actions (H1 ... H6)
533
    for( int i = 1; i <= 6; i++ ) {
534
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
535
      final String markup = String.format( "\n\n%s ", hashes );
536
      final String text = get( "Main.menu.insert.header_" + i );
537
      final String accelerator = "Shortcut+" + i;
538
      final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
539
540
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
541
        e -> getActiveEditor().surroundSelection( markup, "", prompt ),
542
        activeFileEditorIsNull );
543
    }
544
545
    Action insertUnorderedListAction = new Action( get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
546
      e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
547
      activeFileEditorIsNull );
548
    Action insertOrderedListAction = new Action( get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
549
      e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
550
      activeFileEditorIsNull );
551
    Action insertHorizontalRuleAction = new Action( get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
552
      e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
553
      activeFileEditorIsNull );
554
555
    // Help actions
556
    Action helpAboutAction = new Action( get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
557
558
    //---- MenuBar ----
559
    Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ),
560
      fileNewAction,
561
      fileOpenAction,
562
      null,
563
      fileCloseAction,
564
      fileCloseAllAction,
565
      null,
566
      fileSaveAction,
567
      fileSaveAllAction,
568
      null,
569
      fileExitAction );
570
571
    Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ),
572
      editUndoAction,
573
      editRedoAction );
574
575
    Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ),
576
      insertBoldAction,
577
      insertItalicAction,
578
      insertStrikethroughAction,
579
      insertBlockquoteAction,
580
      insertCodeAction,
581
      insertFencedCodeBlockAction,
582
      null,
583
      insertLinkAction,
584
      insertImageAction,
585
      null,
586
      headers[ 0 ],
587
      headers[ 1 ],
588
      headers[ 2 ],
589
      headers[ 3 ],
590
      headers[ 4 ],
591
      headers[ 5 ],
592
      null,
593
      insertUnorderedListAction,
594
      insertOrderedListAction,
595
      insertHorizontalRuleAction );
596
597
    Menu helpMenu = ActionUtils.createMenu( get( "Main.menu.help" ),
598
      helpAboutAction );
599
600
    menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu );
601
602
    //---- ToolBar ----
603
    ToolBar toolBar = ActionUtils.createToolBar(
604
      fileNewAction,
605
      fileOpenAction,
606
      fileSaveAction,
607
      null,
608
      editUndoAction,
609
      editRedoAction,
610
      null,
611
      insertBoldAction,
612
      insertItalicAction,
613
      insertBlockquoteAction,
614
      insertCodeAction,
615
      insertFencedCodeBlockAction,
616
      null,
617
      insertLinkAction,
618
      insertImageAction,
619
      null,
620
      headers[ 0 ],
621
      null,
622
      insertUnorderedListAction,
623
      insertOrderedListAction );
624
625
    return new VBox( menuBar, toolBar );
626
  }
627
628
  /**
629
   * Creates a boolean property that is bound to another boolean value of the
630
   * active editor.
631
   */
632
  private BooleanProperty createActiveBooleanProperty(
633
    final Function<FileEditorTab, ObservableBooleanValue> func ) {
634
635
    final BooleanProperty b = new SimpleBooleanProperty();
636
    final FileEditorTab tab = getActiveFileEditor();
637
638
    if( tab != null ) {
639
      b.bind( func.apply( tab ) );
640
    }
641
642
    getFileEditorPane().activeFileEditorProperty().addListener(
643
      (observable, oldFileEditor, newFileEditor) -> {
644
        b.unbind();
645
646
        if( newFileEditor != null ) {
647
          b.bind( func.apply( newFileEditor ) );
648
        } else {
649
          b.set( false );
650
        }
651
      }
652
    );
653
654
    return b;
625655
  }
626656
M src/main/java/com/scrivenvar/definition/DefinitionFactory.java
3232
import com.scrivenvar.predicates.files.FileTypePredicate;
3333
import com.scrivenvar.service.Settings;
34
import java.io.File;
35
import java.net.MalformedURLException;
36
import java.net.URI;
37
import java.net.URISyntaxException;
38
import java.net.URL;
3439
import java.nio.file.Path;
40
import java.nio.file.Paths;
3541
import java.util.Iterator;
3642
import java.util.List;
...
7177
    final Iterator<String> keys = properties.getKeys( EXTENSIONS_PREFIX );
7278
73
    DefinitionSource definitions = null;
79
    DefinitionSource result = new EmptyDefinitionSource();
7480
7581
    while( keys.hasNext() ) {
7682
      final String key = keys.next();
7783
      final List<String> patterns = properties.getStringSettingList( key );
7884
      final FileTypePredicate predicate = new FileTypePredicate( patterns );
7985
8086
      if( predicate.test( path.toFile() ) ) {
8187
        final String filetype = key.replace( EXTENSIONS_PREFIX + ".", "" );
8288
83
        definitions = createFileDefinitionSource( filetype, path );
89
        result = createFileDefinitionSource( filetype, path );
8490
      }
8591
    }
8692
87
    return definitions;
93
    return result;
94
  }
95
96
  public DefinitionSource createDefinitionSource( final String path ) {
97
98
    final String protocol = getProtocol( path );
99
    DefinitionSource result = new EmptyDefinitionSource();
100
101
    switch( protocol ) {
102
      case "file":
103
        result = fileDefinitionSource( Paths.get( path ) );
104
        break;
105
106
      default:
107
        unknownDefinitionSource( protocol, path );
108
        break;
109
    }
110
111
    return result;
88112
  }
89113
...
98122
  private DefinitionSource createFileDefinitionSource(
99123
    final String filetype, final Path path ) {
100
    final DefinitionSource result;
124
    DefinitionSource result = new EmptyDefinitionSource();
101125
102126
    switch( filetype ) {
103127
      case "yaml":
104128
        result = new YamlFileDefinitionSource( path );
105129
        break;
106130
107131
      default:
108
        result = new EmptyDefinitionSource();
132
        unknownDefinitionSource( filetype, path.toString() );
109133
        break;
110134
    }
111135
112136
    return result;
137
  }
138
139
  /**
140
   * Throws IllegalArgumentException because the given path could not be
141
   * recognized.
142
   *
143
   * @param type The detected path type (protocol, file extension, etc.).
144
   * @param path The path to a source of definitions.
145
   */
146
  private void unknownDefinitionSource( final String type, final String path ) {
147
    throw new IllegalArgumentException(
148
      "Unknown type '" + type + "' for " + path + "."
149
    );
113150
  }
114151
115152
  private Settings getSettings() {
116153
    return this.settings;
154
  }
155
156
  /**
157
   * Returns the protocol for a given URI or filename.
158
   *
159
   * @param source Determine the protocol for this URI or filename.
160
   *
161
   * @return The protocol for the given source.
162
   */
163
  private String getProtocol( final String source ) {
164
    String protocol = null;
165
166
    try {
167
      final URI uri = new URI( source );
168
169
      if( uri.isAbsolute() ) {
170
        protocol = uri.getScheme();
171
      } else {
172
        final URL url = new URL( source );
173
        protocol = url.getProtocol();
174
      }
175
    } catch( final URISyntaxException | MalformedURLException e ) {
176
      // Could be HTTP, HTTPS?
177
      if( source.startsWith( "//" ) ) {
178
        throw new IllegalArgumentException( "Relative context: " + source );
179
      } else {
180
        final File file = new File( source );
181
        protocol = getProtocol( file );
182
      }
183
    }
184
185
    return protocol;
186
  }
187
188
  /**
189
   * Returns the protocol for a given file.
190
   *
191
   * @param file Determine the protocol for this file.
192
   *
193
   * @return The protocol for the given file.
194
   */
195
  private String getProtocol( final File file ) {
196
    String result;
197
198
    try {
199
      result = file.toURI().toURL().getProtocol();
200
    } catch( Exception e ) {
201
      result = "unknown";
202
    }
203
204
    return result;
117205
  }
118206
}
M src/main/java/com/scrivenvar/definition/DefinitionPane.java
6767
  }
6868
69
  /**
70
   * Changes the root node of the tree view. Swaps the current root node for the
71
   * root node of the given
72
   *
73
   * @param treeView The treeview containing a new root node; if the parameter
74
   * is null, the tree is cleared.
75
   */
76
  public void setRoot( final TreeView<String> treeView ) {
77
    getTreeView().setRoot( treeView == null ? null : treeView.getRoot() );
78
  }
79
80
  /**
81
   * Clears the treeview by setting the root node to null.
82
   */
6983
  public void clear() {
70
    getTreeView().setRoot( null );
84
    setRoot( null );
7185
  }
7286
...
321335
   * @param treeView
322336
   */
323
  private void setTreeView( TreeView<String> treeView ) {
337
  private void setTreeView( final TreeView<String> treeView ) {
324338
    if( treeView != null ) {
325339
      this.treeView = treeView;
M src/main/java/com/scrivenvar/definition/DefinitionSource.java
4949
   */
5050
  public TreeView<String> asTreeView() throws IOException;
51
  
51
5252
  /**
5353
   * Returns all the strings with their values resolved in a flat hierarchy.
5454
   * This copies all the keys and resolved values into a new map.
5555
   *
5656
   * @return The new map created with all values having been resolved,
5757
   * recursively.
5858
   */
5959
  public Map<String, String> getResolvedMap();
60
61
  /**
62
   * Must return a re-loadable path to the data source. For a file, this is the
63
   * absolute file path. For a database, this could be the JDBC connection. For
64
   * a web site, this might be the GET url.
65
   *
66
   * @return A non-null, non-empty string.
67
   */
68
  @Override
69
  public String toString();
6070
}
6171
M src/main/java/com/scrivenvar/definition/yaml/YamlParser.java
168168
    final JsonNode rootNode, final String path, final Map<String, String> map ) {
169169
170
    rootNode.fields().forEachRemaining(
171
      (Entry<String, JsonNode> leaf) -> resolve( leaf, path, map )
172
    );
170
    if( rootNode != null ) {
171
      rootNode.fields().forEachRemaining(
172
        (Entry<String, JsonNode> leaf) -> resolve( leaf, path, map )
173
      );
174
    }
173175
  }
174176
M src/main/java/com/scrivenvar/editors/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.util.Lists.getFirst;
66
import static com.scrivenvar.util.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;
7365
7466
/**
M src/main/java/com/scrivenvar/editors/markdown/MarkdownEditorPane.java
2828
package com.scrivenvar.editors.markdown;
2929
30
import static com.scrivenvar.Constants.STYLESHEET_MARKDOWN;
3031
import com.scrivenvar.dialogs.ImageDialog;
3132
import com.scrivenvar.dialogs.LinkDialog;
...
4647
import javafx.stage.Window;
4748
import org.fxmisc.richtext.StyleClassedTextArea;
48
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
49
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
50
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
51
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
52
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
53
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
54
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
55
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
56
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
57
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
58
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
59
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
60
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
61
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
62
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
63
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
64
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
65
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
66
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
67
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
68
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
69
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
70
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
71
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
72
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
73
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
74
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
75
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
76
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
77
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
78
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
79
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
80
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
81
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
82
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
83
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
84
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
85
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
86
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
87
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
88
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
89
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
90
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
91
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
92
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
93
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
94
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
95
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
96
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
97
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
98
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
99
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
100
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
101
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
102
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
103
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
104
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
105
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
106
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
107
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
108
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
109
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
110
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
111
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
112
import static com.scrivenvar.Constants.STYLESHEET_MARKDOWN;
113
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
114
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
115
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
116
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
117
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
118
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
119
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
120
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
121
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
122
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
123
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
124
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
125
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
126
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
127
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
128
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
129
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
130
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
131
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
132
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
133
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
134
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
135
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
136
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
137
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
138
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
139
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
140
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
141
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
142
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
143
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
144
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
145
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
146
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
147
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
148
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
149
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
150
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
151
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
152
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
153
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
154
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
155
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
156
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
157
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
158
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
159
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
160
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
161
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
162
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
163
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
164
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
165
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
166
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
167
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
168
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
169
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
170
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
171
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
172
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
173
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
174
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
175
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
176
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
177
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
178
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
179
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
180
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
181
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
182
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
183
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
184
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
185
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
186
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
187
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
188
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
189
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
190
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
191
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
192
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
193
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
194
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
195
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
196
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
197
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
198
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
199
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
200
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
201
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
202
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
203
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
204
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
205
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
206
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
207
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
208
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
209
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
210
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
211
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
212
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
213
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
214
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
215
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
216
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
217
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
218
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
219
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
220
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
221
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
222
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
223
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
224
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
225
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
226
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
227
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
228
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
229
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
230
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
231
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
232
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
233
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
234
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
235
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
236
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
237
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
238
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
239
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
240
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
241
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
242
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
243
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
244
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
245
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
246
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
247
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
248
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
249
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
250
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
251
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
252
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
253
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
254
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
255
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
256
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
257
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
258
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
259
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
260
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
261
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
262
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
263
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
264
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
265
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
266
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
267
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
268
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
269
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
270
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
271
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
272
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
273
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
274
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
275
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
276
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
277
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
278
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
279
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
280
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
281
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
282
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
283
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
284
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
285
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
286
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
287
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
288
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
289
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
290
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
291
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
292
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
293
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
294
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
295
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
296
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
297
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
298
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
299
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
300
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
301
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
302
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
303
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
30449
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
30550
M src/main/java/com/scrivenvar/processors/MarkdownCaretInsertionProcessor.java
3030
import static com.scrivenvar.Constants.CARET_POSITION_MD;
3131
import static java.lang.Character.isLetter;
32
import static java.lang.Math.min;
3233
3334
/**
...
6566
  @Override
6667
  public String processLink( final String t ) {
67
    int offset = getCaretPosition();
6868
    final int length = t.length();
69
    int offset = min( getCaretPosition(), length );
70
71
    // TODO: Ensure that the caret position is outside of an element, 
72
    // so that a caret inserted in the image doesn't corrupt it. Such as:
73
    //
74
    // ![Screenshot](images/scr|eenshot.png)
75
    //
76
    // 1. Scan back to the previous EOL, which will be the MD AST start point.
77
    // 2. Scan forward until EOF or EOL, which will be the MD AST ending point.
78
    // 3. Convert the text between start and end into MD AST.
79
    // 4. Find the nearest text node to the caret.
80
    // 5. Insert the CARET_POSITION_MD value in the text at that offsset.
6981
7082
    // Insert the caret at the closest non-Markdown delimiter (i.e., the 
7183
    // closest character from the caret position forward).
7284
    while( offset < length && !isLetter( t.charAt( offset ) ) ) {
7385
      offset++;
7486
    }
7587
76
    // TODO: Ensure that the caret position is outside of an element, 
77
    // so that a caret inserted in the image doesn't corrupt it. Such as:
78
    //
79
    // ![Screenshot](images/scr|eenshot.png)
8088
    // Insert the caret position into the Markdown text, but don't interfere
8189
    // with the Markdown iteself.
M src/main/java/com/scrivenvar/processors/text/StringUtilsReplacer.java
2929
3030
import java.util.Map;
31
import static org.apache.commons.lang.StringUtils.replaceEach;
31
import static org.apache.commons.lang3.StringUtils.replaceEach;
3232
3333
/**
M src/main/java/com/scrivenvar/service/Settings.java
6161
6262
  /**
63
   * Returns a setting property or its default value.
64
   *
65
   * @param property The property key name to obtain its value.
66
   * @param defaults The default values to return iff the property cannot be
67
   * found.
68
   *
69
   * @return The property values for the given property key.
70
   */
71
  public List<Object> getSettingList( String property, List<String> defaults );
72
73
  /**
7463
   * Returns a list of property names that begin with the given prefix. The
7564
   * prefix is included in any matching results. This will return keys that
M src/main/java/com/scrivenvar/service/impl/DefaultOptions.java
2828
2929
import static com.scrivenvar.Constants.PREFS_ROOT;
30
import static com.scrivenvar.Constants.PREFS_ROOT_OPTIONS;
31
import static com.scrivenvar.Constants.PREFS_ROOT_STATE;
3230
import com.scrivenvar.service.Options;
3331
import java.util.prefs.Preferences;
3432
import static java.util.prefs.Preferences.userRoot;
33
import static com.scrivenvar.Constants.PREFS_STATE;
34
import static com.scrivenvar.Constants.PREFS_OPTIONS;
3535
3636
/**
...
4343
  
4444
  public DefaultOptions() {
45
    setPreferences( getRootPreferences().node( PREFS_ROOT_OPTIONS ) );
45
    setPreferences(getRootPreferences().node(PREFS_OPTIONS ) );
4646
  }
4747
...
6666
  @Override
6767
  public Preferences getState() {
68
    return getRootPreferences().node( PREFS_ROOT_STATE );
68
    return getRootPreferences().node(PREFS_STATE );
6969
  }
7070
M src/main/java/com/scrivenvar/service/impl/DefaultSettings.java
3131
import com.scrivenvar.service.Settings;
3232
import java.io.IOException;
33
import java.io.InputStreamReader;
34
import java.io.Reader;
3335
import java.net.URISyntaxException;
3436
import java.net.URL;
35
import java.util.ArrayList;
3637
import java.util.Iterator;
3738
import java.util.List;
38
import java.util.Objects;
39
import java.util.stream.Collectors;
40
import org.apache.commons.configuration.ConfigurationException;
41
import org.apache.commons.configuration.PropertiesConfiguration;
39
import org.apache.commons.configuration2.PropertiesConfiguration;
40
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
41
import org.apache.commons.configuration2.convert.ListDelimiterHandler;
42
import org.apache.commons.configuration2.ex.ConfigurationException;
4243
4344
/**
4445
 * Responsible for loading settings that help avoid hard-coded assumptions.
4546
 *
4647
 * @author White Magic Software, Ltd.
4748
 */
4849
public class DefaultSettings implements Settings {
50
  private static final char VALUE_SEPARATOR = ',';
4951
5052
  private PropertiesConfiguration properties;
...
7981
  public int getSetting( final String property, final int defaultValue ) {
8082
    return getSettings().getInt( property, defaultValue );
81
  }
82
83
  /**
84
   * Returns a list of objects for a given setting.
85
   *
86
   * @param property The setting key name.
87
   * @param defaults The default values to return, which may be null.
88
   *
89
   * @return A list, possibly empty, never null.
90
   */
91
  @Override
92
  public List<Object> getSettingList( final String property, List<String> defaults ) {
93
    if( defaults == null ) {
94
      defaults = new ArrayList<>();
95
    }
96
97
    return getSettings().getList( property, defaults );
9883
  }
9984
...
10994
  public List<String> getStringSettingList(
11095
    final String property, final List<String> defaults ) {
111
    final List<Object> settings = getSettingList( property, defaults );
112
113
    return settings.stream()
114
      .map( object -> Objects.toString( object, null ) )
115
      .collect( Collectors.toList() );
96
    return getSettings().getList( String.class, property, defaults );
11697
  }
11798
...
142123
  private PropertiesConfiguration createProperties()
143124
    throws ConfigurationException {
125
144126
    final URL url = getPropertySource();
127
    final PropertiesConfiguration configuration = new PropertiesConfiguration();
145128
146
    return url == null
147
      ? new PropertiesConfiguration()
148
      : new PropertiesConfiguration( url );
129
    if( url != null ) {
130
      try( final Reader r = new InputStreamReader( url.openStream() ) ) {
131
        configuration.setListDelimiterHandler( createListDelimiterHandler() );
132
        configuration.read( r );
133
134
      } catch( IOException e ) {
135
        throw new ConfigurationException( e );
136
      }
137
    }
138
139
    return configuration;
140
  }
141
  
142
  protected ListDelimiterHandler createListDelimiterHandler() {
143
    return new DefaultListDelimiterHandler( VALUE_SEPARATOR );
149144
  }
150145
A src/main/java/com/scrivenvar/test/TestProperties.java
1
package com.scrivenvar.test;
2
3
import java.io.IOException;
4
import java.io.StringReader;
5
import java.util.Arrays;
6
import org.apache.commons.configuration2.PropertiesConfiguration;
7
import org.apache.commons.configuration2.ex.ConfigurationException;
8
9
public class TestProperties {
10
11
  public static void main( final String args[] ) throws ConfigurationException, IOException {
12
    final String p = ""
13
      + "file.ext.definition.yaml=*.yml,*.yaml\n"
14
      + "filter.file.ext.definition=${file.ext.definition.yaml}\n";
15
16
    try( final StringReader r = new StringReader( p ) ) {
17
18
      PropertiesConfiguration config = new PropertiesConfiguration();
19
      config.read( r );
20
21
      System.out.println( config.getList( "filter.file.ext.definition" ) );
22
      System.out.println( config.getString( "filter.file.ext.definition" ) );
23
      System.out.println( Arrays.toString( config.getStringArray( "filter.file.ext.definition" ) ) );
24
    }
25
  }
26
}
127
M src/main/java/com/scrivenvar/test/TestVariableNameProcessor.java
4646
import org.ahocorasick.trie.*;
4747
import org.ahocorasick.trie.Trie.TrieBuilder;
48
import static org.apache.commons.lang.RandomStringUtils.randomNumeric;
49
import org.apache.commons.lang.StringUtils;
48
import static org.apache.commons.lang3.RandomStringUtils.randomNumeric;
49
import org.apache.commons.lang3.StringUtils;
5050
5151
/**
M src/main/resources/com/scrivenvar/settings.properties
1818
preferences.root.state=state
1919
preferences.root.options=options
20
preferences.root.definition.source=definition.source
2021
2122
# ########################################################################