Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M build.gradle
3030
  compile 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
3131
  compile 'org.apache.commons:commons-configuration2:2.1'
32
  compile files('libs/renjin-script-engine-0.8.2309-jar-with-dependencies.jar')
32
  compile files('libs/renjin-script-engine-0.8.2320-jar-with-dependencies.jar')
3333
}
3434
35
version = '1.1.3'
35
version = '1.1.4'
3636
applicationName = 'scrivenvar'
3737
mainClassName = 'com.scrivenvar.Main'
D libs/renjin-script-engine-0.8.2309-jar-with-dependencies.jar
Binary file
A libs/renjin-script-engine-0.8.2320-jar-with-dependencies.jar
Binary file
M src/main/java/com/scrivenvar/Constants.java
4848
  }
4949
50
  private static int get( final String key, int defaultValue ) {
50
  private static int get( final String key, final int defaultValue ) {
5151
    return SETTINGS.getSetting( key, defaultValue );
5252
  }
...
7474
  public static final String FILE_LOGO_256 = get( "file.logo.256" );
7575
  public static final String FILE_LOGO_512 = get( "file.logo.512" );
76
77
  public static final String FILE_R_STARTUP = get( "file.r.startup" );
7876
7977
  public static final String CARET_POSITION_BASE = get( "caret.token.base" );
8078
  public static final String CARET_POSITION_MD = get( "caret.token.markdown" );
8179
  public static final String CARET_POSITION_HTML = get( "caret.token.html" );
8280
8381
  public static final String PREFS_ROOT = get( "preferences.root" );
8482
  public static final String PREFS_STATE = get( "preferences.root.state" );
8583
  public static final String PREFS_OPTIONS = get( "preferences.root.options" );
86
  public static final String PREFS_DEFINITION_SOURCE = get( "preferences.root.definitionSource" );
8784
8885
  // Refer to filename extension settings in the configuration file. Do not
...
10198
  public static final String STATUS_BAR_LINE = "Main.statusbar.line";
10299
  // "OK" text
103
  public static final String STATUS_BAR_DEFAULT = Messages.get( "Main.statusbar.state.default" );
100
  public static final String STATUS_BAR_DEFAULT = get( "Main.statusbar.state.default" );
104101
  public static final String STATUS_PARSE_ERROR = "Main.statusbar.parse.error";
102
103
  // Persistent storage settings.
104
  
105
  /**
106
   * Location of the definition source file.
107
   */
108
  public static final String PERSIST_DEFINITION_SOURCE = "definitionSource";
109
  
105110
  
111
  /**
112
   * Content of the R startup script.
113
   */
114
  public static final String PERSIST_R_STARTUP = "rStartup";
106115
}
107116
M src/main/java/com/scrivenvar/FileEditorTabPane.java
6868
import org.fxmisc.wellbehaved.event.EventPattern;
6969
import org.fxmisc.wellbehaved.event.InputMap;
70
import static com.scrivenvar.Messages.get;
71
import static com.scrivenvar.Messages.get;
72
import static com.scrivenvar.Messages.get;
73
import static com.scrivenvar.Messages.get;
74
import static com.scrivenvar.Messages.get;
75
import static com.scrivenvar.Messages.get;
76
import static com.scrivenvar.Messages.get;
7077
7178
/**
M src/main/java/com/scrivenvar/Main.java
2929
3030
import static com.scrivenvar.Constants.*;
31
import static com.scrivenvar.Messages.get;
3132
import com.scrivenvar.preferences.FilePreferencesFactory;
3233
import com.scrivenvar.service.Options;
...
3940
import javafx.scene.image.Image;
4041
import javafx.stage.Stage;
42
import static com.scrivenvar.Messages.get;
43
import static com.scrivenvar.Messages.get;
44
import static com.scrivenvar.Messages.get;
4145
4246
/**
...
185189
186190
  private String getApplicationTitle() {
187
    return Messages.get( "Main.title" );
191
    return get( "Main.title" );
188192
  }
189193
M src/main/java/com/scrivenvar/MainWindow.java
3131
import static com.scrivenvar.Messages.get;
3232
import com.scrivenvar.definition.*;
33
import com.scrivenvar.editors.EditorPane;
34
import com.scrivenvar.editors.VariableNameInjector;
35
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
36
import com.scrivenvar.predicates.files.FileTypePredicate;
37
import com.scrivenvar.preview.HTMLPreviewPane;
38
import com.scrivenvar.processors.Processor;
39
import com.scrivenvar.processors.ProcessorFactory;
40
import com.scrivenvar.service.Options;
41
import com.scrivenvar.service.Settings;
42
import com.scrivenvar.service.Snitch;
43
import com.scrivenvar.service.events.Notifier;
44
import com.scrivenvar.util.Action;
45
import com.scrivenvar.util.ActionUtils;
46
import static com.scrivenvar.util.StageState.*;
47
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
48
import java.io.IOException;
49
import java.nio.file.Path;
50
import java.util.HashMap;
51
import java.util.Map;
52
import java.util.Observable;
53
import java.util.Observer;
54
import java.util.Optional;
55
import java.util.function.Function;
56
import java.util.prefs.Preferences;
57
import javafx.application.Platform;
58
import javafx.beans.binding.Bindings;
59
import javafx.beans.binding.BooleanBinding;
60
import javafx.beans.property.BooleanProperty;
61
import javafx.beans.property.SimpleBooleanProperty;
62
import javafx.beans.value.ObservableBooleanValue;
63
import javafx.beans.value.ObservableValue;
64
import javafx.collections.ListChangeListener.Change;
65
import javafx.collections.ObservableList;
66
import static javafx.event.Event.fireEvent;
67
import javafx.geometry.Insets;
68
import javafx.geometry.Pos;
69
import javafx.scene.Node;
70
import javafx.scene.Scene;
71
import javafx.scene.control.Alert;
72
import javafx.scene.control.Alert.AlertType;
73
import javafx.scene.control.ButtonBar.ButtonData;
74
import javafx.scene.control.ButtonType;
75
import javafx.scene.control.Dialog;
76
import javafx.scene.control.Menu;
77
import javafx.scene.control.MenuBar;
78
import javafx.scene.control.SplitPane;
79
import javafx.scene.control.Tab;
80
import javafx.scene.control.TextArea;
81
import javafx.scene.control.TextField;
82
import javafx.scene.control.ToolBar;
83
import javafx.scene.control.TreeView;
84
import javafx.scene.image.Image;
85
import javafx.scene.image.ImageView;
86
import static javafx.scene.input.KeyCode.ESCAPE;
87
import javafx.scene.input.KeyEvent;
88
import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
89
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
90
import javafx.scene.layout.BorderPane;
91
import javafx.scene.layout.GridPane;
92
import javafx.scene.layout.VBox;
93
import javafx.scene.text.Text;
94
import javafx.stage.Window;
95
import javafx.stage.WindowEvent;
96
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
97
import org.controlsfx.control.StatusBar;
98
import org.fxmisc.richtext.model.TwoDimensional.Position;
99
100
/**
101
 * Main window containing a tab pane in the center for file editors.
102
 *
103
 * @author Karl Tauber and White Magic Software, Ltd.
104
 */
105
public class MainWindow implements Observer {
106
107
  private final Options options = Services.load( Options.class );
108
  private final Settings settings = Services.load( Settings.class );
109
  private final Snitch snitch = Services.load( Snitch.class );
110
  private final Notifier notifier = Services.load( Notifier.class );
111
112
  private Scene scene;
113
  private MenuBar menuBar;
114
  private StatusBar statusBar;
115
  private Text lineNumberText;
116
  private TextField findTextField;
117
118
  private DefinitionSource definitionSource;
119
  private DefinitionPane definitionPane;
120
  private FileEditorTabPane fileEditorPane;
121
  private HTMLPreviewPane previewPane;
122
123
  /**
124
   * Prevent re-instantiation processing classes.
125
   */
126
  private Map<FileEditorTab, Processor<String>> processors;
127
128
  public MainWindow() {
129
    initLayout();
130
    initFindInput();
131
    initSnitch();
132
    initDefinitionListener();
133
    initTabAddedListener();
134
    initTabChangedListener();
135
    initPreferences();
136
  }
137
138
  public Settings getSettings() {
139
    return settings;
140
  }
141
142
  /**
143
   * Watch for changes to external files. In particular, this awaits
144
   * modifications to any XSL files associated with XML files being edited. When
145
   * an XSL file is modified (external to the application), the snitch's ears
146
   * perk up and the file is reloaded. This keeps the XSL transformation up to
147
   * date with what's on the file system.
148
   */
149
  private void initSnitch() {
150
    getSnitch().addObserver( this );
151
  }
152
153
  /**
154
   * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
155
   * presses.
156
   */
157
  private void initFindInput() {
158
    final TextField input = getFindTextField();
159
160
    input.setOnKeyPressed( (KeyEvent event) -> {
161
      switch( event.getCode() ) {
162
        case F3:
163
        case ENTER:
164
          findNext();
165
          break;
166
        case F:
167
          if( !event.isControlDown() ) {
168
            break;
169
          }
170
        case ESCAPE:
171
          getStatusBar().setGraphic( null );
172
          getActiveFileEditor().getEditorPane().requestFocus();
173
          break;
174
      }
175
    } );
176
177
    // Remove when the input field loses focus.
178
    input.focusedProperty().addListener(
179
      (
180
        final ObservableValue<? extends Boolean> focused,
181
        final Boolean oFocus,
182
        final Boolean nFocus) -> {
183
        if( !nFocus ) {
184
          getStatusBar().setGraphic( null );
185
        }
186
      }
187
    );
188
  }
189
190
  /**
191
   * Listen for file editor tab pane to receive an open definition source event.
192
   */
193
  private void initDefinitionListener() {
194
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
195
      (ObservableValue<? extends Path> definitionFile,
196
        final Path oldPath, final Path newPath) -> {
197
        openDefinition( newPath );
198
199
        // Indirectly refresh the resolved map.
200
        setProcessors( null );
201
202
        updateDefinitionPane();
203
204
        try {
205
          getSnitch().ignore( oldPath );
206
          getSnitch().listen( newPath );
207
        } catch( final IOException ex ) {
208
          error( ex );
209
        }
210
211
        // Will create new processors and therefore a new resolved map.
212
        refreshSelectedTab( getActiveFileEditor() );
213
      }
214
    );
215
  }
216
217
  /**
218
   * When tabs are added, hook the various change listeners onto the new tab so
219
   * that the preview pane refreshes as necessary.
220
   */
221
  private void initTabAddedListener() {
222
    final FileEditorTabPane editorPane = getFileEditorPane();
223
224
    // Make sure the text processor kicks off when new files are opened.
225
    final ObservableList<Tab> tabs = editorPane.getTabs();
226
227
    // Update the preview pane on tab changes.
228
    tabs.addListener(
229
      (final Change<? extends Tab> change) -> {
230
        while( change.next() ) {
231
          if( change.wasAdded() ) {
232
            // Multiple tabs can be added simultaneously.
233
            for( final Tab newTab : change.getAddedSubList() ) {
234
              final FileEditorTab tab = (FileEditorTab)newTab;
235
236
              initTextChangeListener( tab );
237
              initCaretParagraphListener( tab );
238
              initVariableNameInjector( tab );
239
//              initSyntaxListener( tab );
240
            }
241
          }
242
        }
243
      }
244
    );
245
  }
246
247
  /**
248
   * Reloads the preferences from the previous load.
249
   */
250
  private void initPreferences() {
251
    restoreDefinitionSource();
252
    getFileEditorPane().restorePreferences();
253
    updateDefinitionPane();
254
  }
255
256
  /**
257
   * Listen for new tab selection events.
258
   */
259
  private void initTabChangedListener() {
260
    final FileEditorTabPane editorPane = getFileEditorPane();
261
262
    // Update the preview pane changing tabs.
263
    editorPane.addTabSelectionListener(
264
      (ObservableValue<? extends Tab> tabPane,
265
        final Tab oldTab, final Tab newTab) -> {
266
267
        // If there was no old tab, then this is a first time load, which
268
        // can be ignored.
269
        if( oldTab != null ) {
270
          if( newTab == null ) {
271
            closeRemainingTab();
272
          }
273
          else {
274
            // Update the preview with the edited text.
275
            refreshSelectedTab( (FileEditorTab)newTab );
276
          }
277
        }
278
      }
279
    );
280
  }
281
282
  private void initTextChangeListener( final FileEditorTab tab ) {
283
    tab.addTextChangeListener(
284
      (ObservableValue<? extends String> editor,
285
        final String oldValue, final String newValue) -> {
286
        refreshSelectedTab( tab );
287
      }
288
    );
289
  }
290
291
  private void initCaretParagraphListener( final FileEditorTab tab ) {
292
    tab.addCaretParagraphListener(
293
      (ObservableValue<? extends Integer> editor,
294
        final Integer oldValue, final Integer newValue) -> {
295
        refreshSelectedTab( tab );
296
      }
297
    );
298
  }
299
300
  private void initVariableNameInjector( final FileEditorTab tab ) {
301
    VariableNameInjector.listen( tab, getDefinitionPane() );
302
  }
303
304
  /**
305
   * Called whenever the preview pane becomes out of sync with the file editor
306
   * tab. This can be called when the text changes, the caret paragraph changes,
307
   * or the file tab changes.
308
   *
309
   * @param tab The file editor tab that has been changed in some fashion.
310
   */
311
  private void refreshSelectedTab( final FileEditorTab tab ) {
312
    if( tab.isFileOpen() ) {
313
      getPreviewPane().setPath( tab.getPath() );
314
315
      // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
316
      final Position p = tab.getCaretOffset();
317
      getLineNumberText().setText(
318
        get( STATUS_BAR_LINE,
319
          p.getMajor() + 1,
320
          p.getMinor() + 1,
321
          tab.getCaretPosition() + 1
322
        )
323
      );
324
325
      Processor<String> processor = getProcessors().get( tab );
326
327
      if( processor == null ) {
328
        processor = createProcessor( tab );
329
        getProcessors().put( tab, processor );
330
      }
331
332
      try {
333
        getNotifier().clear();
334
        processor.processChain( tab.getEditorText() );
335
      } catch( final Exception ex ) {
336
        error( ex );
337
      }
338
    }
339
  }
340
341
  /**
342
   * Used to find text in the active file editor window.
343
   */
344
  private void find() {
345
    final TextField input = getFindTextField();
346
    getStatusBar().setGraphic( input );
347
    input.requestFocus();
348
  }
349
350
  public void findNext() {
351
    getActiveFileEditor().searchNext( getFindTextField().getText() );
352
  }
353
354
  /**
355
   * Returns the variable map of interpolated definitions.
356
   *
357
   * @return A map to help dereference variables.
358
   */
359
  private Map<String, String> getResolvedMap() {
360
    return getDefinitionSource().getResolvedMap();
361
  }
362
363
  /**
364
   * Returns the root node for the hierarchical definition source.
365
   *
366
   * @return Data to display in the definition pane.
367
   */
368
  private TreeView<String> getTreeView() {
369
    try {
370
      return getDefinitionSource().asTreeView();
371
    } catch( Exception e ) {
372
      error( e );
373
    }
374
375
    // Slightly redundant as getDefinitionSource() might have returned an
376
    // empty definition source.
377
    return (new EmptyDefinitionSource()).asTreeView();
378
  }
379
380
  /**
381
   * Called when a definition source is opened.
382
   *
383
   * @param path Path to the definition source that was opened.
384
   */
385
  private void openDefinition( final Path path ) {
386
    try {
387
      final DefinitionSource ds = createDefinitionSource( path.toString() );
388
      setDefinitionSource( ds );
389
      storeDefinitionSource();
390
      updateDefinitionPane();
391
    } catch( final Exception e ) {
392
      error( e );
393
    }
394
  }
395
396
  private void updateDefinitionPane() {
397
    getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
398
  }
399
400
  private void restoreDefinitionSource() {
401
    final Preferences preferences = getPreferences();
402
    final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
403
404
    // If there's no definition source set, don't try to load it.
405
    if( source != null ) {
406
      setDefinitionSource( createDefinitionSource( source ) );
407
    }
408
  }
409
410
  private void storeDefinitionSource() {
411
    final Preferences preferences = getPreferences();
412
    final DefinitionSource ds = getDefinitionSource();
413
414
    preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
415
  }
416
417
  /**
418
   * Called when the last open tab is closed to clear the preview pane.
419
   */
420
  private void closeRemainingTab() {
421
    getPreviewPane().clear();
422
  }
423
424
  /**
425
   * Called when an exception occurs that warrants the user's attention.
426
   *
427
   * @param e The exception with a message that the user should know about.
428
   */
429
  private void error( final Exception e ) {
430
    getNotifier().notify( e );
431
  }
432
433
  //---- File actions -------------------------------------------------------
434
  /**
435
   * Called when an observable instance has changed. This is called by both the
436
   * snitch service and the notify service. The snitch service can be called for
437
   * different file types, including definition sources.
438
   *
439
   * @param observable The observed instance.
440
   * @param value The noteworthy item.
441
   */
442
  @Override
443
  public void update( final Observable observable, final Object value ) {
444
    if( value != null ) {
445
      if( observable instanceof Snitch && value instanceof Path ) {
446
        final Path path = (Path)value;
447
        final FileTypePredicate predicate
448
          = new FileTypePredicate( GLOB_DEFINITION_EXTENSIONS );
449
450
        // Reload definitions.
451
        if( predicate.test( path.toFile() ) ) {
452
          updateDefinitionSource( path );
453
        }
454
455
        updateSelectedTab();
456
      }
457
      else if( observable instanceof Notifier && value instanceof String ) {
458
        updateStatusBar( (String)value );
459
      }
460
    }
461
  }
462
463
  /**
464
   * Updates the status bar to show the given message.
465
   *
466
   * @param s The message to show in the status bar.
467
   */
468
  private void updateStatusBar( final String s ) {
469
    Platform.runLater(
470
      () -> {
471
        final int index = s.indexOf( '\n' );
472
        final String message = s.substring( 0, index > 0 ? index : s.length() );
473
474
        getStatusBar().setText( message );
475
      }
476
    );
477
  }
478
479
  /**
480
   * Called when a file has been modified.
481
   *
482
   * @param file Path to the modified file.
483
   */
484
  private void updateSelectedTab() {
485
    Platform.runLater(
486
      () -> {
487
        // Brute-force XSLT file reload by re-instantiating all processors.
488
        resetProcessors();
489
        refreshSelectedTab( getActiveFileEditor() );
490
      }
491
    );
492
  }
493
494
  /**
495
   * Reloads the definition source from the given path.
496
   *
497
   * @param path The path containing new definition information.
498
   */
499
  private void updateDefinitionSource( final Path path ) {
500
    Platform.runLater(
501
      () -> {
502
        openDefinition( path );
503
      }
504
    );
505
  }
506
507
  /**
508
   * After resetting the processors, they will refresh anew to be up-to-date
509
   * with the files (text and definition) currently loaded into the editor.
510
   */
511
  private void resetProcessors() {
512
    getProcessors().clear();
513
  }
514
515
  //---- File actions -------------------------------------------------------
516
  private void fileNew() {
517
    getFileEditorPane().newEditor();
518
  }
519
520
  private void fileOpen() {
521
    getFileEditorPane().openFileDialog();
522
  }
523
524
  private void fileClose() {
525
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
526
  }
527
528
  private void fileCloseAll() {
529
    getFileEditorPane().closeAllEditors();
530
  }
531
532
  private void fileSave() {
533
    getFileEditorPane().saveEditor( getActiveFileEditor() );
534
  }
535
536
  private void fileSaveAll() {
537
    getFileEditorPane().saveAllEditors();
538
  }
539
540
  private void fileExit() {
541
    final Window window = getWindow();
542
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
543
  }
544
545
  //---- Help actions -------------------------------------------------------
546
  private void helpAbout() {
547
    Alert alert = new Alert( AlertType.INFORMATION );
548
    alert.setTitle( get( "Dialog.about.title" ) );
549
    alert.setHeaderText( get( "Dialog.about.header" ) );
550
    alert.setContentText( get( "Dialog.about.content" ) );
551
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
552
    alert.initOwner( getWindow() );
553
554
    alert.showAndWait();
555
  }
556
557
  //---- Convenience accessors ----------------------------------------------
558
  private float getFloat( final String key, final float defaultValue ) {
559
    return getPreferences().getFloat( key, defaultValue );
560
  }
561
562
  private Preferences getPreferences() {
563
    return getOptions().getState();
564
  }
565
566
  private TextField createFindTextField() {
567
    return new TextField();
568
  }
569
570
  private void toolsScript() {
571
    try {
572
      // Create a custom dialog.
573
      Dialog<String> dialog = new Dialog<>();
574
      dialog.setTitle( "R Startup Script" );
575
576
      final String script = getSettings().loadRStartupScript();
577
578
      final ButtonType saveButton = new ButtonType( "Save", ButtonData.OK_DONE );
579
      dialog.getDialogPane().getButtonTypes().addAll( saveButton, ButtonType.CANCEL );
580
581
      GridPane grid = new GridPane();
582
      grid.setHgap( 10 );
583
      grid.setVgap( 10 );
584
      grid.setPadding( new Insets( 20, 100, 10, 10 ) );
585
586
      final TextArea textArea = new TextArea( script );
587
      textArea.setEditable( true );
588
      textArea.setWrapText( true );
589
590
      grid.add( textArea, 0, 0 );
591
      dialog.getDialogPane().setContent( grid );
592
593
      Platform.runLater( () -> textArea.requestFocus() );
594
595
      dialog.setResultConverter( button -> {
596
        return (button == saveButton) ? textArea.getText() : "";
597
      } );
598
599
      final Optional<String> result = dialog.showAndWait();
600
601
      result.ifPresent( s -> {
602
        try {
603
          getSettings().saveRStartupScript( s );
604
        } catch( IOException ex ) {
605
          getNotifier().notify( ex );
606
        }
607
      } );
608
609
    } catch( final IOException ex ) {
610
      getNotifier().notify( ex );
611
    }
612
  }
613
614
  protected Scene getScene() {
615
    if( this.scene == null ) {
616
      this.scene = createScene();
617
    }
618
619
    return this.scene;
620
  }
621
622
  public Window getWindow() {
623
    return getScene().getWindow();
624
  }
625
626
  private MarkdownEditorPane getActiveEditor() {
627
    final EditorPane pane = getActiveFileEditor().getEditorPane();
628
629
    return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
630
  }
631
632
  private FileEditorTab getActiveFileEditor() {
633
    return getFileEditorPane().getActiveFileEditor();
634
  }
635
636
  //---- Member accessors ---------------------------------------------------
637
  private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) {
638
    this.processors = map;
639
  }
640
641
  private Map<FileEditorTab, Processor<String>> getProcessors() {
642
    if( this.processors == null ) {
643
      setProcessors( new HashMap<>() );
644
    }
645
646
    return this.processors;
647
  }
648
649
  private FileEditorTabPane getFileEditorPane() {
650
    if( this.fileEditorPane == null ) {
651
      this.fileEditorPane = createFileEditorPane();
652
    }
653
654
    return this.fileEditorPane;
655
  }
656
657
  private HTMLPreviewPane getPreviewPane() {
658
    if( this.previewPane == null ) {
659
      this.previewPane = createPreviewPane();
660
    }
661
662
    return this.previewPane;
663
  }
664
665
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
666
    this.definitionSource = definitionSource;
667
  }
668
669
  private DefinitionSource getDefinitionSource() {
670
    if( this.definitionSource == null ) {
671
      this.definitionSource = new EmptyDefinitionSource();
672
    }
673
674
    return this.definitionSource;
675
  }
676
677
  private DefinitionPane getDefinitionPane() {
678
    if( this.definitionPane == null ) {
679
      this.definitionPane = createDefinitionPane();
680
    }
681
682
    return this.definitionPane;
683
  }
684
685
  private Options getOptions() {
686
    return this.options;
687
  }
688
689
  private Snitch getSnitch() {
690
    return this.snitch;
691
  }
692
693
  private Notifier getNotifier() {
694
    return this.notifier;
695
  }
696
697
  public void setMenuBar( final MenuBar menuBar ) {
698
    this.menuBar = menuBar;
699
  }
700
701
  public MenuBar getMenuBar() {
702
    return this.menuBar;
703
  }
704
705
  private Text getLineNumberText() {
706
    if( this.lineNumberText == null ) {
707
      this.lineNumberText = createLineNumberText();
708
    }
709
710
    return this.lineNumberText;
711
  }
712
713
  private synchronized StatusBar getStatusBar() {
714
    if( this.statusBar == null ) {
715
      this.statusBar = createStatusBar();
716
    }
717
718
    return this.statusBar;
719
  }
720
721
  private TextField getFindTextField() {
722
    if( this.findTextField == null ) {
723
      this.findTextField = createFindTextField();
724
    }
725
726
    return this.findTextField;
727
  }
728
729
  //---- Member creators ----------------------------------------------------
730
  /**
731
   * Factory to create processors that are suited to different file types.
732
   *
733
   * @param tab The tab that is subjected to processing.
734
   *
735
   * @return A processor suited to the file type specified by the tab's path.
736
   */
737
  private Processor<String> createProcessor( final FileEditorTab tab ) {
738
    return createProcessorFactory().createProcessor( tab );
739
  }
740
741
  private ProcessorFactory createProcessorFactory() {
742
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
743
  }
744
745
  private DefinitionSource createDefinitionSource( final String path ) {
746
    final DefinitionSource ds
747
      = createDefinitionFactory().createDefinitionSource( path );
748
749
    if( ds instanceof FileDefinitionSource ) {
750
      try {
751
        getSnitch().listen( ((FileDefinitionSource)ds).getPath() );
752
      } catch( final IOException ex ) {
753
        error( ex );
754
      }
755
    }
756
757
    return ds;
758
  }
759
760
  /**
761
   * Create an editor pane to hold file editor tabs.
762
   *
763
   * @return A new instance, never null.
764
   */
765
  private FileEditorTabPane createFileEditorPane() {
766
    return new FileEditorTabPane();
767
  }
768
769
  private HTMLPreviewPane createPreviewPane() {
770
    return new HTMLPreviewPane();
771
  }
772
773
  private DefinitionPane createDefinitionPane() {
774
    return new DefinitionPane( getTreeView() );
775
  }
776
777
  private DefinitionFactory createDefinitionFactory() {
778
    return new DefinitionFactory();
779
  }
780
781
  private StatusBar createStatusBar() {
782
    return new StatusBar();
783
  }
784
785
  private Scene createScene() {
786
    final SplitPane splitPane = new SplitPane(
787
      getDefinitionPane().getNode(),
788
      getFileEditorPane().getNode(),
789
      getPreviewPane().getNode() );
790
791
    splitPane.setDividerPositions(
792
      getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
793
      getFloat( K_PANE_SPLIT_EDITOR, .45f ),
794
      getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
795
796
    // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
797
    final BorderPane borderPane = new BorderPane();
798
    borderPane.setPrefSize( 1024, 800 );
799
    borderPane.setTop( createMenuBar() );
800
    borderPane.setBottom( getStatusBar() );
801
    borderPane.setCenter( splitPane );
802
803
    final VBox box = new VBox();
804
    box.setAlignment( Pos.BASELINE_CENTER );
805
    box.getChildren().add( getLineNumberText() );
806
    getStatusBar().getRightItems().add( box );
807
808
    return new Scene( borderPane );
809
  }
810
811
  private Text createLineNumberText() {
812
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
813
  }
814
815
  private Node createMenuBar() {
816
    final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
817
818
    // File actions
819
    final Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
820
    final Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
821
    final Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
822
    final Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
823
    final Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
824
      createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
825
    final Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
826
      Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
827
    final Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
828
829
    // Edit actions
830
    final Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
831
      e -> getActiveEditor().undo(),
832
      createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
833
    final Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
834
      e -> getActiveEditor().redo(),
835
      createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
836
    final Action editFindAction = new Action( Messages.get( "Main.menu.edit.find" ), "Ctrl+F", SEARCH,
837
      e -> find(),
838
      activeFileEditorIsNull );
839
    final Action editReplaceAction = new Action( Messages.get( "Main.menu.edit.find.replace" ), "Shortcut+H", RETWEET,
840
      e -> getActiveEditor().replace(),
841
      activeFileEditorIsNull );
842
    final Action editFindNextAction = new Action( Messages.get( "Main.menu.edit.find.next" ), "F3", null,
843
      e -> findNext(),
844
      activeFileEditorIsNull );
845
    final Action editFindPreviousAction = new Action( Messages.get( "Main.menu.edit.find.previous" ), "Shift+F3", null,
846
      e -> getActiveEditor().findPrevious(),
847
      activeFileEditorIsNull );
848
849
    // Insert actions
850
    final Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
851
      e -> getActiveEditor().surroundSelection( "**", "**" ),
852
      activeFileEditorIsNull );
853
    final Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
854
      e -> getActiveEditor().surroundSelection( "*", "*" ),
855
      activeFileEditorIsNull );
856
    final Action insertSuperscriptAction = new Action( get( "Main.menu.insert.superscript" ), "Shortcut+[", SUPERSCRIPT,
857
      e -> getActiveEditor().surroundSelection( "^", "^" ),
858
      activeFileEditorIsNull );
859
    final Action insertSubscriptAction = new Action( get( "Main.menu.insert.subscript" ), "Shortcut+]", SUBSCRIPT,
860
      e -> getActiveEditor().surroundSelection( "~", "~" ),
861
      activeFileEditorIsNull );
862
    final Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
863
      e -> getActiveEditor().surroundSelection( "~~", "~~" ),
864
      activeFileEditorIsNull );
865
    final Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
866
      e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
867
      activeFileEditorIsNull );
868
    final Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
869
      e -> getActiveEditor().surroundSelection( "`", "`" ),
870
      activeFileEditorIsNull );
871
    final Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
872
      e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ),
873
      activeFileEditorIsNull );
874
875
    final Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
876
      e -> getActiveEditor().insertLink(),
877
      activeFileEditorIsNull );
878
    final Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT,
879
      e -> getActiveEditor().insertImage(),
880
      activeFileEditorIsNull );
881
882
    final Action[] headers = new Action[ 6 ];
883
884
    // Insert header actions (H1 ... H6)
885
    for( int i = 1; i <= 6; i++ ) {
886
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
887
      final String markup = String.format( "%n%n%s ", hashes );
888
      final String text = get( "Main.menu.insert.header_" + i );
889
      final String accelerator = "Shortcut+" + i;
890
      final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
891
892
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
893
        e -> getActiveEditor().surroundSelection( markup, "", prompt ),
894
        activeFileEditorIsNull );
895
    }
896
897
    final Action insertUnorderedListAction = new Action(
898
      get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
899
      e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
900
      activeFileEditorIsNull );
901
    final Action insertOrderedListAction = new Action(
902
      get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
903
      e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
904
      activeFileEditorIsNull );
905
    final Action insertHorizontalRuleAction = new Action(
906
      get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
907
      e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
908
      activeFileEditorIsNull );
909
910
    // Tools actions
911
    final Action toolsScriptAction = new Action(
912
      get( "Main.menu.tools.script" ), null, null, e -> toolsScript() );
913
914
    // Help actions
915
    final Action helpAboutAction = new Action(
916
      get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
917
918
    //---- MenuBar ----
919
    final Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ),
920
      fileNewAction,
921
      fileOpenAction,
922
      null,
923
      fileCloseAction,
924
      fileCloseAllAction,
925
      null,
926
      fileSaveAction,
927
      fileSaveAllAction,
928
      null,
929
      fileExitAction );
930
931
    final Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ),
932
      editUndoAction,
933
      editRedoAction,
934
      editFindAction,
935
      editReplaceAction,
936
      editFindNextAction,
937
      editFindPreviousAction );
938
939
    final Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ),
940
      insertBoldAction,
941
      insertItalicAction,
942
      insertSuperscriptAction,
943
      insertSubscriptAction,
944
      insertStrikethroughAction,
945
      insertBlockquoteAction,
946
      insertCodeAction,
947
      insertFencedCodeBlockAction,
948
      null,
949
      insertLinkAction,
950
      insertImageAction,
951
      null,
952
      headers[ 0 ],
953
      headers[ 1 ],
954
      headers[ 2 ],
955
      headers[ 3 ],
956
      headers[ 4 ],
957
      headers[ 5 ],
958
      null,
959
      insertUnorderedListAction,
960
      insertOrderedListAction,
961
      insertHorizontalRuleAction );
962
963
    final Menu toolsMenu = ActionUtils.createMenu( get( "Main.menu.tools" ),
964
      toolsScriptAction );
965
966
    final Menu helpMenu = ActionUtils.createMenu( get( "Main.menu.help" ),
967
      helpAboutAction );
968
969
    menuBar = new MenuBar( fileMenu, editMenu, insertMenu, toolsMenu, helpMenu );
970
971
    //---- ToolBar ----
972
    ToolBar toolBar = ActionUtils.createToolBar(
973
      fileNewAction,
974
      fileOpenAction,
975
      fileSaveAction,
976
      null,
977
      editUndoAction,
978
      editRedoAction,
979
      null,
980
      insertBoldAction,
981
      insertItalicAction,
982
      insertSuperscriptAction,
983
      insertSubscriptAction,
984
      insertBlockquoteAction,
985
      insertCodeAction,
986
      insertFencedCodeBlockAction,
987
      null,
988
      insertLinkAction,
989
      insertImageAction,
990
      null,
991
      headers[ 0 ],
992
      null,
993
      insertUnorderedListAction,
994
      insertOrderedListAction );
995
996
    return new VBox( menuBar, toolBar );
997
  }
998
999
  /**
1000
   * Creates a boolean property that is bound to another boolean value of the
1001
   * active editor.
1002
   */
1003
  private BooleanProperty createActiveBooleanProperty(
1004
    final Function<FileEditorTab, ObservableBooleanValue> func ) {
1005
1006
    final BooleanProperty b = new SimpleBooleanProperty();
1007
    final FileEditorTab tab = getActiveFileEditor();
1008
1009
    if( tab != null ) {
1010
      b.bind( func.apply( tab ) );
1011
    }
1012
1013
    getFileEditorPane().activeFileEditorProperty().addListener(
1014
      (observable, oldFileEditor, newFileEditor) -> {
1015
        b.unbind();
1016
1017
        if( newFileEditor != null ) {
1018
          b.bind( func.apply( newFileEditor ) );
1019
        }
1020
        else {
1021
          b.set( false );
1022
        }
1023
      }
1024
    );
1025
1026
    return b;
1027
  }
1028
1029
  private void initLayout() {
1030
    final Scene appScene = getScene();
1031
1032
    appScene.getStylesheets().add( STYLESHEET_SCENE );
1033
//    appScene.getStylesheets().add( STYLESHEET_XML );
1034
33
import com.scrivenvar.dialogs.RScriptDialog;
34
import com.scrivenvar.editors.EditorPane;
35
import com.scrivenvar.editors.VariableNameInjector;
36
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
37
import com.scrivenvar.predicates.files.FileTypePredicate;
38
import com.scrivenvar.preview.HTMLPreviewPane;
39
import com.scrivenvar.processors.Processor;
40
import com.scrivenvar.processors.ProcessorFactory;
41
import com.scrivenvar.service.Options;
42
import com.scrivenvar.service.Snitch;
43
import com.scrivenvar.service.events.Notifier;
44
import com.scrivenvar.util.Action;
45
import com.scrivenvar.util.ActionUtils;
46
import static com.scrivenvar.util.StageState.*;
47
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
48
import java.io.IOException;
49
import java.nio.file.Path;
50
import java.util.HashMap;
51
import java.util.Map;
52
import java.util.Observable;
53
import java.util.Observer;
54
import java.util.Optional;
55
import java.util.function.Function;
56
import java.util.prefs.Preferences;
57
import javafx.application.Platform;
58
import javafx.beans.binding.Bindings;
59
import javafx.beans.binding.BooleanBinding;
60
import javafx.beans.property.BooleanProperty;
61
import javafx.beans.property.SimpleBooleanProperty;
62
import javafx.beans.value.ObservableBooleanValue;
63
import javafx.beans.value.ObservableValue;
64
import javafx.collections.ListChangeListener.Change;
65
import javafx.collections.ObservableList;
66
import static javafx.event.Event.fireEvent;
67
import javafx.geometry.Pos;
68
import javafx.scene.Node;
69
import javafx.scene.Scene;
70
import javafx.scene.control.Alert;
71
import javafx.scene.control.Alert.AlertType;
72
import javafx.scene.control.Menu;
73
import javafx.scene.control.MenuBar;
74
import javafx.scene.control.SplitPane;
75
import javafx.scene.control.Tab;
76
import javafx.scene.control.TextField;
77
import javafx.scene.control.ToolBar;
78
import javafx.scene.control.TreeView;
79
import javafx.scene.image.Image;
80
import javafx.scene.image.ImageView;
81
import static javafx.scene.input.KeyCode.ESCAPE;
82
import javafx.scene.input.KeyEvent;
83
import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
84
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
85
import javafx.scene.layout.BorderPane;
86
import javafx.scene.layout.VBox;
87
import javafx.scene.text.Text;
88
import javafx.stage.Window;
89
import javafx.stage.WindowEvent;
90
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
91
import org.controlsfx.control.StatusBar;
92
import org.fxmisc.richtext.model.TwoDimensional.Position;
93
94
/**
95
 * Main window containing a tab pane in the center for file editors.
96
 *
97
 * @author Karl Tauber and White Magic Software, Ltd.
98
 */
99
public class MainWindow implements Observer {
100
101
  private final Options options = Services.load( Options.class );
102
  private final Snitch snitch = Services.load( Snitch.class );
103
  private final Notifier notifier = Services.load( Notifier.class );
104
105
  private Scene scene;
106
  private MenuBar menuBar;
107
  private StatusBar statusBar;
108
  private Text lineNumberText;
109
  private TextField findTextField;
110
111
  private DefinitionSource definitionSource;
112
  private DefinitionPane definitionPane;
113
  private FileEditorTabPane fileEditorPane;
114
  private HTMLPreviewPane previewPane;
115
116
  /**
117
   * Prevents re-instantiation of processing classes.
118
   */
119
  private Map<FileEditorTab, Processor<String>> processors;
120
121
  public MainWindow() {
122
    initLayout();
123
    initFindInput();
124
    initSnitch();
125
    initDefinitionListener();
126
    initTabAddedListener();
127
    initTabChangedListener();
128
    initPreferences();
129
  }
130
131
  /**
132
   * Watch for changes to external files. In particular, this awaits
133
   * modifications to any XSL files associated with XML files being edited. When
134
   * an XSL file is modified (external to the application), the snitch's ears
135
   * perk up and the file is reloaded. This keeps the XSL transformation up to
136
   * date with what's on the file system.
137
   */
138
  private void initSnitch() {
139
    getSnitch().addObserver( this );
140
  }
141
142
  /**
143
   * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
144
   * presses.
145
   */
146
  private void initFindInput() {
147
    final TextField input = getFindTextField();
148
149
    input.setOnKeyPressed( (KeyEvent event) -> {
150
      switch( event.getCode() ) {
151
        case F3:
152
        case ENTER:
153
          findNext();
154
          break;
155
        case F:
156
          if( !event.isControlDown() ) {
157
            break;
158
          }
159
        case ESCAPE:
160
          getStatusBar().setGraphic( null );
161
          getActiveFileEditor().getEditorPane().requestFocus();
162
          break;
163
      }
164
    } );
165
166
    // Remove when the input field loses focus.
167
    input.focusedProperty().addListener(
168
      (
169
        final ObservableValue<? extends Boolean> focused,
170
        final Boolean oFocus,
171
        final Boolean nFocus) -> {
172
        if( !nFocus ) {
173
          getStatusBar().setGraphic( null );
174
        }
175
      }
176
    );
177
  }
178
179
  /**
180
   * Listen for file editor tab pane to receive an open definition source event.
181
   */
182
  private void initDefinitionListener() {
183
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
184
      (ObservableValue<? extends Path> definitionFile,
185
        final Path oldPath, final Path newPath) -> {
186
        openDefinition( newPath );
187
188
        // Indirectly refresh the resolved map.
189
        setProcessors( null );
190
        updateDefinitionPane();
191
192
        try {
193
          getSnitch().ignore( oldPath );
194
          getSnitch().listen( newPath );
195
        } catch( final IOException ex ) {
196
          error( ex );
197
        }
198
199
        // Will create new processors and therefore a new resolved map.
200
        refreshSelectedTab( getActiveFileEditor() );
201
      }
202
    );
203
  }
204
205
  /**
206
   * When tabs are added, hook the various change listeners onto the new tab so
207
   * that the preview pane refreshes as necessary.
208
   */
209
  private void initTabAddedListener() {
210
    final FileEditorTabPane editorPane = getFileEditorPane();
211
212
    // Make sure the text processor kicks off when new files are opened.
213
    final ObservableList<Tab> tabs = editorPane.getTabs();
214
215
    // Update the preview pane on tab changes.
216
    tabs.addListener(
217
      (final Change<? extends Tab> change) -> {
218
        while( change.next() ) {
219
          if( change.wasAdded() ) {
220
            // Multiple tabs can be added simultaneously.
221
            for( final Tab newTab : change.getAddedSubList() ) {
222
              final FileEditorTab tab = (FileEditorTab)newTab;
223
224
              initTextChangeListener( tab );
225
              initCaretParagraphListener( tab );
226
              initVariableNameInjector( tab );
227
//              initSyntaxListener( tab );
228
            }
229
          }
230
        }
231
      }
232
    );
233
  }
234
235
  /**
236
   * Reloads the preferences from the previous session.
237
   */
238
  private void initPreferences() {
239
    restoreDefinitionSource();
240
    getFileEditorPane().restorePreferences();
241
    updateDefinitionPane();
242
  }
243
244
  /**
245
   * Listen for new tab selection events.
246
   */
247
  private void initTabChangedListener() {
248
    final FileEditorTabPane editorPane = getFileEditorPane();
249
250
    // Update the preview pane changing tabs.
251
    editorPane.addTabSelectionListener(
252
      (ObservableValue<? extends Tab> tabPane,
253
        final Tab oldTab, final Tab newTab) -> {
254
255
        // If there was no old tab, then this is a first time load, which
256
        // can be ignored.
257
        if( oldTab != null ) {
258
          if( newTab == null ) {
259
            closeRemainingTab();
260
          }
261
          else {
262
            // Update the preview with the edited text.
263
            refreshSelectedTab( (FileEditorTab)newTab );
264
          }
265
        }
266
      }
267
    );
268
  }
269
270
  private void initTextChangeListener( final FileEditorTab tab ) {
271
    tab.addTextChangeListener(
272
      (ObservableValue<? extends String> editor,
273
        final String oldValue, final String newValue) -> {
274
        refreshSelectedTab( tab );
275
      }
276
    );
277
  }
278
279
  private void initCaretParagraphListener( final FileEditorTab tab ) {
280
    tab.addCaretParagraphListener(
281
      (ObservableValue<? extends Integer> editor,
282
        final Integer oldValue, final Integer newValue) -> {
283
        refreshSelectedTab( tab );
284
      }
285
    );
286
  }
287
288
  private void initVariableNameInjector( final FileEditorTab tab ) {
289
    VariableNameInjector.listen( tab, getDefinitionPane() );
290
  }
291
292
  /**
293
   * Called whenever the preview pane becomes out of sync with the file editor
294
   * tab. This can be called when the text changes, the caret paragraph changes,
295
   * or the file tab changes.
296
   *
297
   * @param tab The file editor tab that has been changed in some fashion.
298
   */
299
  private void refreshSelectedTab( final FileEditorTab tab ) {
300
    if( tab.isFileOpen() ) {
301
      getPreviewPane().setPath( tab.getPath() );
302
303
      // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
304
      final Position p = tab.getCaretOffset();
305
      getLineNumberText().setText(
306
        get( STATUS_BAR_LINE,
307
          p.getMajor() + 1,
308
          p.getMinor() + 1,
309
          tab.getCaretPosition() + 1
310
        )
311
      );
312
313
      Processor<String> processor = getProcessors().get( tab );
314
315
      if( processor == null ) {
316
        processor = createProcessor( tab );
317
        getProcessors().put( tab, processor );
318
      }
319
320
      try {
321
        getNotifier().clear();
322
        processor.processChain( tab.getEditorText() );
323
      } catch( final Exception ex ) {
324
        error( ex );
325
      }
326
    }
327
  }
328
329
  /**
330
   * Used to find text in the active file editor window.
331
   */
332
  private void find() {
333
    final TextField input = getFindTextField();
334
    getStatusBar().setGraphic( input );
335
    input.requestFocus();
336
  }
337
338
  public void findNext() {
339
    getActiveFileEditor().searchNext( getFindTextField().getText() );
340
  }
341
342
  /**
343
   * Returns the variable map of interpolated definitions.
344
   *
345
   * @return A map to help dereference variables.
346
   */
347
  private Map<String, String> getResolvedMap() {
348
    return getDefinitionSource().getResolvedMap();
349
  }
350
351
  /**
352
   * Returns the root node for the hierarchical definition source.
353
   *
354
   * @return Data to display in the definition pane.
355
   */
356
  private TreeView<String> getTreeView() {
357
    try {
358
      return getDefinitionSource().asTreeView();
359
    } catch( Exception e ) {
360
      error( e );
361
    }
362
363
    // Slightly redundant as getDefinitionSource() might have returned an
364
    // empty definition source.
365
    return (new EmptyDefinitionSource()).asTreeView();
366
  }
367
368
  /**
369
   * Called when a definition source is opened.
370
   *
371
   * @param path Path to the definition source that was opened.
372
   */
373
  private void openDefinition( final Path path ) {
374
    try {
375
      final DefinitionSource ds = createDefinitionSource( path.toString() );
376
      setDefinitionSource( ds );
377
      storeDefinitionSource();
378
      updateDefinitionPane();
379
    } catch( final Exception e ) {
380
      error( e );
381
    }
382
  }
383
384
  private void updateDefinitionPane() {
385
    getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
386
  }
387
388
  private void restoreDefinitionSource() {
389
    final Preferences preferences = getPreferences();
390
    final String source = preferences.get( PERSIST_DEFINITION_SOURCE, null );
391
392
    // If there's no definition source set, don't try to load it.
393
    if( source != null ) {
394
      setDefinitionSource( createDefinitionSource( source ) );
395
    }
396
  }
397
398
  private void storeDefinitionSource() {
399
    final Preferences preferences = getPreferences();
400
    final DefinitionSource ds = getDefinitionSource();
401
402
    preferences.put( PERSIST_DEFINITION_SOURCE, ds.toString() );
403
  }
404
405
  /**
406
   * Called when the last open tab is closed to clear the preview pane.
407
   */
408
  private void closeRemainingTab() {
409
    getPreviewPane().clear();
410
  }
411
412
  /**
413
   * Called when an exception occurs that warrants the user's attention.
414
   *
415
   * @param e The exception with a message that the user should know about.
416
   */
417
  private void error( final Exception e ) {
418
    getNotifier().notify( e );
419
  }
420
421
  //---- File actions -------------------------------------------------------
422
  /**
423
   * Called when an observable instance has changed. This is called by both the
424
   * snitch service and the notify service. The snitch service can be called for
425
   * different file types, including definition sources.
426
   *
427
   * @param observable The observed instance.
428
   * @param value The noteworthy item.
429
   */
430
  @Override
431
  public void update( final Observable observable, final Object value ) {
432
    if( value != null ) {
433
      if( observable instanceof Snitch && value instanceof Path ) {
434
        final Path path = (Path)value;
435
        final FileTypePredicate predicate
436
          = new FileTypePredicate( GLOB_DEFINITION_EXTENSIONS );
437
438
        // Reload definitions.
439
        if( predicate.test( path.toFile() ) ) {
440
          updateDefinitionSource( path );
441
        }
442
443
        updateSelectedTab();
444
      }
445
      else if( observable instanceof Notifier && value instanceof String ) {
446
        updateStatusBar( (String)value );
447
      }
448
    }
449
  }
450
451
  /**
452
   * Updates the status bar to show the given message.
453
   *
454
   * @param s The message to show in the status bar.
455
   */
456
  private void updateStatusBar( final String s ) {
457
    Platform.runLater(
458
      () -> {
459
        final int index = s.indexOf( '\n' );
460
        final String message = s.substring( 0, index > 0 ? index : s.length() );
461
462
        getStatusBar().setText( message );
463
      }
464
    );
465
  }
466
467
  /**
468
   * Called when a file has been modified.
469
   *
470
   * @param file Path to the modified file.
471
   */
472
  private void updateSelectedTab() {
473
    Platform.runLater(
474
      () -> {
475
        // Brute-force XSLT file reload by re-instantiating all processors.
476
        resetProcessors();
477
        refreshSelectedTab( getActiveFileEditor() );
478
      }
479
    );
480
  }
481
482
  /**
483
   * Reloads the definition source from the given path.
484
   *
485
   * @param path The path containing new definition information.
486
   */
487
  private void updateDefinitionSource( final Path path ) {
488
    Platform.runLater(
489
      () -> {
490
        openDefinition( path );
491
      }
492
    );
493
  }
494
495
  /**
496
   * After resetting the processors, they will refresh anew to be up-to-date
497
   * with the files (text and definition) currently loaded into the editor.
498
   */
499
  private void resetProcessors() {
500
    getProcessors().clear();
501
  }
502
503
  //---- File actions -------------------------------------------------------
504
  private void fileNew() {
505
    getFileEditorPane().newEditor();
506
  }
507
508
  private void fileOpen() {
509
    getFileEditorPane().openFileDialog();
510
  }
511
512
  private void fileClose() {
513
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
514
  }
515
516
  private void fileCloseAll() {
517
    getFileEditorPane().closeAllEditors();
518
  }
519
520
  private void fileSave() {
521
    getFileEditorPane().saveEditor( getActiveFileEditor() );
522
  }
523
524
  private void fileSaveAll() {
525
    getFileEditorPane().saveAllEditors();
526
  }
527
528
  private void fileExit() {
529
    final Window window = getWindow();
530
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
531
  }
532
533
  //---- Tools actions
534
  private void toolsScript() {
535
    final String script = getStartupScript();
536
537
    System.out.println( "script = '" + script + "'" );
538
539
    final RScriptDialog dialog = new RScriptDialog(
540
      getWindow(), "Dialog.rScript.title", script );
541
    final Optional<String> result = dialog.showAndWait();
542
543
    result.ifPresent( (String s) -> {
544
      putStartupScript( s );
545
    } );
546
  }
547
548
  /**
549
   * Gets the R startup script from the user preferences.
550
   */
551
  private String getStartupScript() {
552
    return getPreferences().get( PERSIST_R_STARTUP, "" );
553
  }
554
555
  /**
556
   * Puts an R startup script into the user preferences.
557
   */
558
  private void putStartupScript( final String s ) {
559
    try {
560
      System.out.println( "put startup script = '" + s + "'" );
561
      getPreferences().put( PERSIST_R_STARTUP, s );
562
      getPreferences().sync();
563
    } catch( final Exception ex ) {
564
      getNotifier().notify( ex );
565
    }
566
  }
567
568
  //---- Help actions -------------------------------------------------------
569
  private void helpAbout() {
570
    Alert alert = new Alert( AlertType.INFORMATION );
571
    alert.setTitle( get( "Dialog.about.title" ) );
572
    alert.setHeaderText( get( "Dialog.about.header" ) );
573
    alert.setContentText( get( "Dialog.about.content" ) );
574
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
575
    alert.initOwner( getWindow() );
576
577
    alert.showAndWait();
578
  }
579
580
  //---- Convenience accessors ----------------------------------------------
581
  private float getFloat( final String key, final float defaultValue ) {
582
    return getPreferences().getFloat( key, defaultValue );
583
  }
584
585
  private Preferences getPreferences() {
586
    return getOptions().getState();
587
  }
588
589
  protected Scene getScene() {
590
    if( this.scene == null ) {
591
      this.scene = createScene();
592
    }
593
594
    return this.scene;
595
  }
596
597
  public Window getWindow() {
598
    return getScene().getWindow();
599
  }
600
601
  private MarkdownEditorPane getActiveEditor() {
602
    final EditorPane pane = getActiveFileEditor().getEditorPane();
603
604
    return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
605
  }
606
607
  private FileEditorTab getActiveFileEditor() {
608
    return getFileEditorPane().getActiveFileEditor();
609
  }
610
611
  //---- Member accessors ---------------------------------------------------
612
  private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) {
613
    this.processors = map;
614
  }
615
616
  private Map<FileEditorTab, Processor<String>> getProcessors() {
617
    if( this.processors == null ) {
618
      setProcessors( new HashMap<>() );
619
    }
620
621
    return this.processors;
622
  }
623
624
  private FileEditorTabPane getFileEditorPane() {
625
    if( this.fileEditorPane == null ) {
626
      this.fileEditorPane = createFileEditorPane();
627
    }
628
629
    return this.fileEditorPane;
630
  }
631
632
  private HTMLPreviewPane getPreviewPane() {
633
    if( this.previewPane == null ) {
634
      this.previewPane = createPreviewPane();
635
    }
636
637
    return this.previewPane;
638
  }
639
640
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
641
    this.definitionSource = definitionSource;
642
  }
643
644
  private DefinitionSource getDefinitionSource() {
645
    if( this.definitionSource == null ) {
646
      this.definitionSource = new EmptyDefinitionSource();
647
    }
648
649
    return this.definitionSource;
650
  }
651
652
  private DefinitionPane getDefinitionPane() {
653
    if( this.definitionPane == null ) {
654
      this.definitionPane = createDefinitionPane();
655
    }
656
657
    return this.definitionPane;
658
  }
659
660
  private Options getOptions() {
661
    return this.options;
662
  }
663
664
  private Snitch getSnitch() {
665
    return this.snitch;
666
  }
667
668
  private Notifier getNotifier() {
669
    return this.notifier;
670
  }
671
672
  public void setMenuBar( final MenuBar menuBar ) {
673
    this.menuBar = menuBar;
674
  }
675
676
  public MenuBar getMenuBar() {
677
    return this.menuBar;
678
  }
679
680
  private Text getLineNumberText() {
681
    if( this.lineNumberText == null ) {
682
      this.lineNumberText = createLineNumberText();
683
    }
684
685
    return this.lineNumberText;
686
  }
687
688
  private synchronized StatusBar getStatusBar() {
689
    if( this.statusBar == null ) {
690
      this.statusBar = createStatusBar();
691
    }
692
693
    return this.statusBar;
694
  }
695
696
  private TextField getFindTextField() {
697
    if( this.findTextField == null ) {
698
      this.findTextField = createFindTextField();
699
    }
700
701
    return this.findTextField;
702
  }
703
704
  //---- Member creators ----------------------------------------------------
705
  /**
706
   * Factory to create processors that are suited to different file types.
707
   *
708
   * @param tab The tab that is subjected to processing.
709
   *
710
   * @return A processor suited to the file type specified by the tab's path.
711
   */
712
  private Processor<String> createProcessor( final FileEditorTab tab ) {
713
    return createProcessorFactory().createProcessor( tab );
714
  }
715
716
  private ProcessorFactory createProcessorFactory() {
717
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
718
  }
719
720
  private DefinitionSource createDefinitionSource( final String path ) {
721
    final DefinitionSource ds
722
      = createDefinitionFactory().createDefinitionSource( path );
723
724
    if( ds instanceof FileDefinitionSource ) {
725
      try {
726
        getSnitch().listen( ((FileDefinitionSource)ds).getPath() );
727
      } catch( final IOException ex ) {
728
        error( ex );
729
      }
730
    }
731
732
    return ds;
733
  }
734
735
  private TextField createFindTextField() {
736
    return new TextField();
737
  }
738
739
  /**
740
   * Create an editor pane to hold file editor tabs.
741
   *
742
   * @return A new instance, never null.
743
   */
744
  private FileEditorTabPane createFileEditorPane() {
745
    return new FileEditorTabPane();
746
  }
747
748
  private HTMLPreviewPane createPreviewPane() {
749
    return new HTMLPreviewPane();
750
  }
751
752
  private DefinitionPane createDefinitionPane() {
753
    return new DefinitionPane( getTreeView() );
754
  }
755
756
  private DefinitionFactory createDefinitionFactory() {
757
    return new DefinitionFactory();
758
  }
759
760
  private StatusBar createStatusBar() {
761
    return new StatusBar();
762
  }
763
764
  private Scene createScene() {
765
    final SplitPane splitPane = new SplitPane(
766
      getDefinitionPane().getNode(),
767
      getFileEditorPane().getNode(),
768
      getPreviewPane().getNode() );
769
770
    splitPane.setDividerPositions(
771
      getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
772
      getFloat( K_PANE_SPLIT_EDITOR, .45f ),
773
      getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
774
775
    // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
776
    final BorderPane borderPane = new BorderPane();
777
    borderPane.setPrefSize( 1024, 800 );
778
    borderPane.setTop( createMenuBar() );
779
    borderPane.setBottom( getStatusBar() );
780
    borderPane.setCenter( splitPane );
781
782
    final VBox box = new VBox();
783
    box.setAlignment( Pos.BASELINE_CENTER );
784
    box.getChildren().add( getLineNumberText() );
785
    getStatusBar().getRightItems().add( box );
786
787
    return new Scene( borderPane );
788
  }
789
790
  private Text createLineNumberText() {
791
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
792
  }
793
794
  private Node createMenuBar() {
795
    final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
796
797
    // File actions
798
    final Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
799
    final Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
800
    final Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
801
    final Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
802
    final Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
803
      createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
804
    final Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
805
      Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
806
    final Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
807
808
    // Edit actions
809
    final Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
810
      e -> getActiveEditor().undo(),
811
      createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
812
    final Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
813
      e -> getActiveEditor().redo(),
814
      createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
815
    final Action editFindAction = new Action( Messages.get( "Main.menu.edit.find" ), "Ctrl+F", SEARCH,
816
      e -> find(),
817
      activeFileEditorIsNull );
818
    final Action editReplaceAction = new Action( Messages.get( "Main.menu.edit.find.replace" ), "Shortcut+H", RETWEET,
819
      e -> getActiveEditor().replace(),
820
      activeFileEditorIsNull );
821
    final Action editFindNextAction = new Action( Messages.get( "Main.menu.edit.find.next" ), "F3", null,
822
      e -> findNext(),
823
      activeFileEditorIsNull );
824
    final Action editFindPreviousAction = new Action( Messages.get( "Main.menu.edit.find.previous" ), "Shift+F3", null,
825
      e -> getActiveEditor().findPrevious(),
826
      activeFileEditorIsNull );
827
828
    // Insert actions
829
    final Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
830
      e -> getActiveEditor().surroundSelection( "**", "**" ),
831
      activeFileEditorIsNull );
832
    final Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
833
      e -> getActiveEditor().surroundSelection( "*", "*" ),
834
      activeFileEditorIsNull );
835
    final Action insertSuperscriptAction = new Action( get( "Main.menu.insert.superscript" ), "Shortcut+[", SUPERSCRIPT,
836
      e -> getActiveEditor().surroundSelection( "^", "^" ),
837
      activeFileEditorIsNull );
838
    final Action insertSubscriptAction = new Action( get( "Main.menu.insert.subscript" ), "Shortcut+]", SUBSCRIPT,
839
      e -> getActiveEditor().surroundSelection( "~", "~" ),
840
      activeFileEditorIsNull );
841
    final Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
842
      e -> getActiveEditor().surroundSelection( "~~", "~~" ),
843
      activeFileEditorIsNull );
844
    final Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
845
      e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
846
      activeFileEditorIsNull );
847
    final Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
848
      e -> getActiveEditor().surroundSelection( "`", "`" ),
849
      activeFileEditorIsNull );
850
    final Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
851
      e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ),
852
      activeFileEditorIsNull );
853
854
    final Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
855
      e -> getActiveEditor().insertLink(),
856
      activeFileEditorIsNull );
857
    final Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT,
858
      e -> getActiveEditor().insertImage(),
859
      activeFileEditorIsNull );
860
861
    final Action[] headers = new Action[ 6 ];
862
863
    // Insert header actions (H1 ... H6)
864
    for( int i = 1; i <= 6; i++ ) {
865
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
866
      final String markup = String.format( "%n%n%s ", hashes );
867
      final String text = get( "Main.menu.insert.header_" + i );
868
      final String accelerator = "Shortcut+" + i;
869
      final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
870
871
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
872
        e -> getActiveEditor().surroundSelection( markup, "", prompt ),
873
        activeFileEditorIsNull );
874
    }
875
876
    final Action insertUnorderedListAction = new Action(
877
      get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
878
      e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
879
      activeFileEditorIsNull );
880
    final Action insertOrderedListAction = new Action(
881
      get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
882
      e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
883
      activeFileEditorIsNull );
884
    final Action insertHorizontalRuleAction = new Action(
885
      get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
886
      e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
887
      activeFileEditorIsNull );
888
889
    // Tools actions
890
    final Action toolsScriptAction = new Action(
891
      get( "Main.menu.tools.script" ), null, null, e -> toolsScript() );
892
893
    // Help actions
894
    final Action helpAboutAction = new Action(
895
      get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
896
897
    //---- MenuBar ----
898
    final Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ),
899
      fileNewAction,
900
      fileOpenAction,
901
      null,
902
      fileCloseAction,
903
      fileCloseAllAction,
904
      null,
905
      fileSaveAction,
906
      fileSaveAllAction,
907
      null,
908
      fileExitAction );
909
910
    final Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ),
911
      editUndoAction,
912
      editRedoAction,
913
      editFindAction,
914
      editReplaceAction,
915
      editFindNextAction,
916
      editFindPreviousAction );
917
918
    final Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ),
919
      insertBoldAction,
920
      insertItalicAction,
921
      insertSuperscriptAction,
922
      insertSubscriptAction,
923
      insertStrikethroughAction,
924
      insertBlockquoteAction,
925
      insertCodeAction,
926
      insertFencedCodeBlockAction,
927
      null,
928
      insertLinkAction,
929
      insertImageAction,
930
      null,
931
      headers[ 0 ],
932
      headers[ 1 ],
933
      headers[ 2 ],
934
      headers[ 3 ],
935
      headers[ 4 ],
936
      headers[ 5 ],
937
      null,
938
      insertUnorderedListAction,
939
      insertOrderedListAction,
940
      insertHorizontalRuleAction );
941
942
    final Menu toolsMenu = ActionUtils.createMenu( get( "Main.menu.tools" ),
943
      toolsScriptAction );
944
945
    final Menu helpMenu = ActionUtils.createMenu( get( "Main.menu.help" ),
946
      helpAboutAction );
947
948
    menuBar = new MenuBar( fileMenu, editMenu, insertMenu, toolsMenu, helpMenu );
949
950
    //---- ToolBar ----
951
    ToolBar toolBar = ActionUtils.createToolBar(
952
      fileNewAction,
953
      fileOpenAction,
954
      fileSaveAction,
955
      null,
956
      editUndoAction,
957
      editRedoAction,
958
      null,
959
      insertBoldAction,
960
      insertItalicAction,
961
      insertSuperscriptAction,
962
      insertSubscriptAction,
963
      insertBlockquoteAction,
964
      insertCodeAction,
965
      insertFencedCodeBlockAction,
966
      null,
967
      insertLinkAction,
968
      insertImageAction,
969
      null,
970
      headers[ 0 ],
971
      null,
972
      insertUnorderedListAction,
973
      insertOrderedListAction );
974
975
    return new VBox( menuBar, toolBar );
976
  }
977
978
  /**
979
   * Creates a boolean property that is bound to another boolean value of the
980
   * active editor.
981
   */
982
  private BooleanProperty createActiveBooleanProperty(
983
    final Function<FileEditorTab, ObservableBooleanValue> func ) {
984
985
    final BooleanProperty b = new SimpleBooleanProperty();
986
    final FileEditorTab tab = getActiveFileEditor();
987
988
    if( tab != null ) {
989
      b.bind( func.apply( tab ) );
990
    }
991
992
    getFileEditorPane().activeFileEditorProperty().addListener(
993
      (observable, oldFileEditor, newFileEditor) -> {
994
        b.unbind();
995
996
        if( newFileEditor != null ) {
997
          b.bind( func.apply( newFileEditor ) );
998
        }
999
        else {
1000
          b.set( false );
1001
        }
1002
      }
1003
    );
1004
1005
    return b;
1006
  }
1007
1008
  private void initLayout() {
1009
    final Scene appScene = getScene();
1010
1011
    appScene.getStylesheets().add( STYLESHEET_SCENE );
1012
1013
    // TODO: Apply an XML syntax highlighting for XML files.
1014
//    appScene.getStylesheets().add( STYLESHEET_XML );
10351015
    appScene.windowProperty().addListener(
10361016
      (observable, oldWindow, newWindow) -> {
A src/main/java/com/scrivenvar/dialogs/AbstractDialog.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.dialogs;
29
30
import static com.scrivenvar.Messages.get;
31
import com.scrivenvar.service.events.impl.ButtonOrderPane;
32
import static javafx.scene.control.ButtonType.CANCEL;
33
import static javafx.scene.control.ButtonType.OK;
34
import javafx.scene.control.Dialog;
35
import javafx.stage.Window;
36
37
/**
38
 * Superclass that abstracts common behaviours for all dialogs.
39
 *
40
 * @author White Magic Software, Ltd.
41
 * @param <T> The type of dialog to create (usually String).
42
 */
43
public abstract class AbstractDialog<T> extends Dialog<T> {
44
45
  /**
46
   * Ensures that all dialogs can be closed.
47
   *
48
   * @param owner The parent window of this dialog.
49
   * @param title The messages title to display in the title bar.
50
   */
51
  @SuppressWarnings( "OverridableMethodCallInConstructor" )
52
  public AbstractDialog( final Window owner, final String title ) {
53
    setTitle( get( title ) );
54
    setResizable( true );
55
56
    initOwner( owner );
57
    initCloseAction();
58
    initDialogPane();
59
    initDialogButtons();
60
    initComponents();
61
  }
62
63
  /**
64
   * Initialize the component layout.
65
   */
66
  protected abstract void initComponents();
67
68
  /**
69
   * Set the dialog to use a button order pane with an OK and a CANCEL button.
70
   */
71
  protected void initDialogPane() {
72
    setDialogPane( new ButtonOrderPane() );
73
  }
74
  
75
  /**
76
   * Set an OK and CANCEL button on the dialog.
77
   */
78
  protected void initDialogButtons() {
79
    getDialogPane().getButtonTypes().addAll( OK, CANCEL );
80
  }
81
82
  /**
83
   * Attaches a setOnCloseRequest to the dialog's [X] button so that the user
84
   * can always close the window, even if there's an error.
85
   */
86
  protected final void initCloseAction() {
87
    final Window window = getDialogPane().getScene().getWindow();
88
    window.setOnCloseRequest( event -> window.hide() );
89
  }
90
}
191
M src/main/java/com/scrivenvar/dialogs/ImageDialog.java
2727
package com.scrivenvar.dialogs;
2828
29
import com.scrivenvar.Messages;
29
import static com.scrivenvar.Messages.get;
3030
import com.scrivenvar.controls.BrowseFileButton;
3131
import com.scrivenvar.controls.EscapeTextField;
32
import com.scrivenvar.service.events.impl.ButtonOrderPane;
3332
import java.nio.file.Path;
3433
import javafx.application.Platform;
3534
import javafx.beans.binding.Bindings;
3635
import javafx.beans.property.SimpleStringProperty;
3736
import javafx.beans.property.StringProperty;
3837
import javafx.scene.control.ButtonBar.ButtonData;
39
import javafx.scene.control.ButtonType;
40
import javafx.scene.control.Dialog;
38
import static javafx.scene.control.ButtonType.OK;
4139
import javafx.scene.control.DialogPane;
4240
import javafx.scene.control.Label;
...
5048
 * @author Karl Tauber
5149
 */
52
public class ImageDialog extends Dialog<String> {
50
public class ImageDialog extends AbstractDialog<String> {
5351
5452
  private final StringProperty image = new SimpleStringProperty();
55
56
  public ImageDialog( Window owner, Path basePath ) {
57
    setTitle( Messages.get( "ImageDialog.title" ) );
58
    initOwner( owner );
59
    setResizable( true );
6053
61
    initComponents();
54
  public ImageDialog( final Window owner, final Path basePath ) {
55
    super(owner, "Dialog.image.title" );
56
    
57
    final DialogPane dialogPane = getDialogPane();
58
    dialogPane.setContent( pane );
6259
6360
    linkBrowseFileButton.setBasePath( basePath );
64
    linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( Messages.get( "ImageDialog.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) );
61
    linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) );
6562
    linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() );
66
67
    setDialogPane( new ButtonOrderPane() );
68
    final DialogPane dialogPane = getDialogPane();
69
    dialogPane.setContent( pane );
70
    dialogPane.getButtonTypes().addAll( ButtonType.OK, ButtonType.CANCEL );
7163
72
    dialogPane.lookupButton( ButtonType.OK ).disableProperty().bind(
64
    dialogPane.lookupButton( OK ).disableProperty().bind(
7365
      urlField.escapedTextProperty().isEmpty()
7466
      .or( textField.escapedTextProperty().isEmpty() ) );
...
9385
  }
9486
95
  private void initComponents() {
87
  @Override
88
  protected void initComponents() {
9689
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
9790
    pane = new MigPane();
...
112105
113106
      //---- urlLabel ----
114
      urlLabel.setText( Messages.get( "ImageDialog.urlLabel.text" ) );
107
      urlLabel.setText( get( "Dialog.image.urlLabel.text" ) );
115108
      pane.add( urlLabel, "cell 0 0" );
116109
...
123116
124117
      //---- textLabel ----
125
      textLabel.setText( Messages.get( "ImageDialog.textLabel.text" ) );
118
      textLabel.setText( get( "Dialog.image.textLabel.text" ) );
126119
      pane.add( textLabel, "cell 0 1" );
127120
128121
      //---- textField ----
129122
      textField.setEscapeCharacters( "[]" );
130123
      pane.add( textField, "cell 1 1 2 1" );
131124
132125
      //---- titleLabel ----
133
      titleLabel.setText( Messages.get( "ImageDialog.titleLabel.text" ) );
126
      titleLabel.setText( get( "Dialog.image.titleLabel.text" ) );
134127
      pane.add( titleLabel, "cell 0 2" );
135128
      pane.add( titleField, "cell 1 2 2 1" );
136129
137130
      //---- previewLabel ----
138
      previewLabel.setText( Messages.get( "ImageDialog.previewLabel.text" ) );
131
      previewLabel.setText( get( "Dialog.image.previewLabel.text" ) );
139132
      pane.add( previewLabel, "cell 0 3" );
140133
      pane.add( previewField, "cell 1 3 2 1" );
M src/main/java/com/scrivenvar/dialogs/LinkDialog.java
2828
package com.scrivenvar.dialogs;
2929
30
import com.scrivenvar.Messages;
30
import static com.scrivenvar.Messages.get;
3131
import com.scrivenvar.controls.EscapeTextField;
3232
import com.scrivenvar.editors.markdown.HyperlinkModel;
33
import com.scrivenvar.service.events.impl.ButtonOrderPane;
3433
import java.nio.file.Path;
3534
import javafx.application.Platform;
3635
import javafx.beans.binding.Bindings;
3736
import javafx.beans.property.SimpleStringProperty;
3837
import javafx.beans.property.StringProperty;
3938
import javafx.scene.control.ButtonBar.ButtonData;
40
import javafx.scene.control.ButtonType;
41
import javafx.scene.control.Dialog;
39
import static javafx.scene.control.ButtonType.OK;
4240
import javafx.scene.control.DialogPane;
4341
import javafx.scene.control.Label;
...
5048
 * @author Karl Tauber
5149
 */
52
public class LinkDialog extends Dialog<String> {
50
public class LinkDialog extends AbstractDialog<String> {
5351
5452
  private final StringProperty link = new SimpleStringProperty();
55
56
  public LinkDialog( final Window owner, final HyperlinkModel hyperlink, final Path basePath ) {
57
    setTitle( Messages.get( "LinkDialog.title" ) );
58
    initOwner( owner );
59
    setResizable( true );
60
61
    initComponents();
6253
63
    setDialogPane( new ButtonOrderPane() );
54
  public LinkDialog(
55
    final Window owner, final HyperlinkModel hyperlink, final Path basePath ) {
56
    super( owner, "Dialog.link.title" );
6457
65
    final DialogPane dialog = getDialogPane();
66
    dialog.setContent( pane );
67
    dialog.getButtonTypes().addAll( ButtonType.OK, ButtonType.CANCEL );
58
    final DialogPane dialogPane = getDialogPane();
59
    dialogPane.setContent( pane );
6860
69
    dialog.lookupButton( ButtonType.OK ).disableProperty().bind(
61
    dialogPane.lookupButton( OK ).disableProperty().bind(
7062
      urlField.escapedTextProperty().isEmpty() );
7163
...
9183
  }
9284
93
  private void initComponents() {
85
  @Override
86
  protected void initComponents() {
9487
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
9588
    pane = new MigPane();
...
107100
108101
      //---- urlLabel ----
109
      urlLabel.setText( Messages.get( "LinkDialog.urlLabel.text" ) );
102
      urlLabel.setText( get( "Dialog.link.urlLabel.text" ) );
110103
      pane.add( urlLabel, "cell 0 0" );
111104
112105
      //---- urlField ----
113106
      urlField.setEscapeCharacters( "()" );
114107
      pane.add( urlField, "cell 1 0" );
115108
116109
      //---- textLabel ----
117
      textLabel.setText( Messages.get( "LinkDialog.textLabel.text" ) );
110
      textLabel.setText( get( "Dialog.link.textLabel.text" ) );
118111
      pane.add( textLabel, "cell 0 1" );
119112
120113
      //---- textField ----
121114
      textField.setEscapeCharacters( "[]" );
122115
      pane.add( textField, "cell 1 1 3 1" );
123116
124117
      //---- titleLabel ----
125
      titleLabel.setText( Messages.get( "LinkDialog.titleLabel.text" ) );
118
      titleLabel.setText( get( "Dialog.link.titleLabel.text" ) );
126119
      pane.add( titleLabel, "cell 0 2" );
127120
      pane.add( titleField, "cell 1 2 3 1" );
A src/main/java/com/scrivenvar/dialogs/RScriptDialog.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.dialogs;
29
30
import static com.scrivenvar.Messages.get;
31
import javafx.application.Platform;
32
import javafx.geometry.Insets;
33
import static javafx.scene.control.ButtonType.OK;
34
import javafx.scene.control.DialogPane;
35
import javafx.scene.control.Label;
36
import javafx.scene.control.TextArea;
37
import javafx.scene.layout.GridPane;
38
import javafx.stage.Window;
39
40
/**
41
 * Responsible for managing the R startup script that is run when an R source
42
 * file is loaded.
43
 *
44
 * @author White Magic Software, Ltd.
45
 */
46
public class RScriptDialog extends AbstractDialog<String> {
47
48
  private TextArea scriptArea;
49
50
  public RScriptDialog(
51
    final Window parent, final String title, final String script ) {
52
    super( parent, title );
53
    getScriptArea().setText( script );
54
  }
55
56
  @Override
57
  protected void initComponents() {
58
    final DialogPane pane = getDialogPane();
59
60
    final GridPane grid = new GridPane();
61
    grid.setHgap( 10 );
62
    grid.setVgap( 10 );
63
    grid.setPadding( new Insets( 10, 10, 10, 10 ) );
64
65
    final Label label = new Label( get( "Dialog.rScript.content" ) );
66
67
    final TextArea textArea = getScriptArea();
68
    textArea.setEditable( true );
69
    textArea.setWrapText( true );
70
71
    grid.add( label, 0, 0 );
72
    grid.add( textArea, 0, 1 );
73
    pane.setContent( grid );
74
75
    Platform.runLater( () -> textArea.requestFocus() );
76
77
    setResultConverter( dialogButton -> {
78
      return dialogButton == OK ? textArea.getText() : "";
79
    } );
80
  }
81
82
  private TextArea getScriptArea() {
83
    if( this.scriptArea == null ) {
84
      this.scriptArea = new TextArea();
85
    }
86
87
    return this.scriptArea;
88
  }
89
}
190
M src/main/java/com/scrivenvar/editors/markdown/MarkdownEditorPane.java
4848
import org.fxmisc.richtext.StyleClassedTextArea;
4949
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;
5053
5154
/**
M src/main/java/com/scrivenvar/processors/InlineRProcessor.java
2828
package com.scrivenvar.processors;
2929
30
import static com.scrivenvar.Constants.PERSIST_R_STARTUP;
3031
import static com.scrivenvar.Constants.STATUS_PARSE_ERROR;
3132
import static com.scrivenvar.Messages.get;
3233
import com.scrivenvar.Services;
3334
import static com.scrivenvar.decorators.RVariableDecorator.PREFIX;
3435
import static com.scrivenvar.decorators.RVariableDecorator.SUFFIX;
3536
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
36
import com.scrivenvar.service.Settings;
37
import com.scrivenvar.service.Options;
3738
import com.scrivenvar.service.events.Notifier;
3839
import java.io.IOException;
3940
import static java.lang.Math.min;
4041
import java.nio.file.Path;
42
import java.nio.file.Paths;
4143
import java.util.Map;
4244
import javax.script.ScriptEngine;
...
5254
5355
  private final Notifier notifier = Services.load( Notifier.class );
54
  private final Settings settings = Services.load( Settings.class );
56
  private final Options options = Services.load( Options.class );
5557
5658
  private ScriptEngine engine;
...
7375
7476
  /**
75
   * @see https://github.com/DaveJarvis/scrivenvar/issues/30
77
   *
7678
   * @param workingDirectory
7779
   */
7880
  private void init( final Path workingDirectory ) {
7981
    try {
80
      final String dir = workingDirectory.toString().replace( '\\', '/' );
82
      final Path wd = nullSafe( workingDirectory );
83
      final String dir = wd.toString().replace( '\\', '/' );
8184
      final Map<String, String> definitions = getDefinitions();
8285
      definitions.put( "$application.r.working.directory$", dir );
8386
8487
      final String initScript = getInitScript();
85
      final String rScript = replace( initScript, getDefinitions() );
88
      System.out.println( "script = '" + initScript + "'" );
8689
87
      eval( rScript );
90
      if( !initScript.isEmpty() ) {
91
        final String rScript = replace( initScript, getDefinitions() );
92
        eval( rScript );
93
      }
8894
    } catch( final IOException | ScriptException e ) {
8995
      throw new RuntimeException( e );
9096
    }
9197
  }
9298
9399
  private String getInitScript() throws IOException {
94
    return getSettings().loadRStartupScript();
100
    return getOptions().get( PERSIST_R_STARTUP, "" );
95101
  }
96102
...
180186
  }
181187
182
  private Settings getSettings() {
183
    return this.settings;
188
  private Options getOptions() {
189
    return this.options;
190
  }
191
192
  /**
193
   * This will return the given path if not null, otherwise it will return
194
   * Paths.get( System.getProperty( "user.dir" ) ).
195
   *
196
   * @param path The path to make null safe.
197
   *
198
   * @return A non-null path.
199
   */
200
  private Path nullSafe( final Path path ) {
201
    return path == null ? Paths.get( System.getProperty( "user.dir" ) ) : path;
184202
  }
185203
}
M src/main/java/com/scrivenvar/service/Options.java
2828
package com.scrivenvar.service;
2929
30
import java.util.prefs.BackingStoreException;
3031
import java.util.prefs.Preferences;
3132
...
3839
3940
  public Preferences getState();
40
  
41
4142
  /**
4243
   * Stores the key and value into the user preferences to be loaded the next
4344
   * time the application is launched.
4445
   *
4546
   * @param key Name of the key to persist along with its value.
4647
   * @param value Value to associate with the key.
48
   *
49
   * @throws BackingStoreException Could not persist the change.
4750
   */
48
  public void put( String key, String value );
51
  public void put( String key, String value ) throws BackingStoreException;
4952
5053
  /**
...
5861
   */
5962
  public String get( String key, String defaultValue );
63
  
64
  /**
65
   * Retrieves the value for a key in the user preferences. This will return
66
   * the empty string if the value cannot be found.
67
   * 
68
   * @param key The key to find in the preferences.
69
   * @return A non-null, possibly empty value for the key.
70
   */
71
  public String get( String key );
6072
}
6173
M src/main/java/com/scrivenvar/service/Settings.java
2828
package com.scrivenvar.service;
2929
30
import java.io.IOException;
3130
import java.util.Iterator;
3231
import java.util.List;
...
9493
  public List<String> getStringSettingList( String property );
9594
96
  /**
97
   * Reads the R startup script into a string, or the empty string if the file
98
   * could not be read (or found). The R startup file must be UTF-8.
99
   *
100
   * @return The string content for the R startup script, or empty if not found.
101
   *
102
   * @throws IOException Could not read the R startup script.
103
   */
104
  public String loadRStartupScript() throws IOException;
10595
10696
  /**
107
   * Writes the R startup script into its predefined location.
97
   * Changes key's value. This will clear the old value before setting the
98
   * new value so that the old value is erased, not changed into a list.
10899
   *
109
   * @param script The string content for the R startup script.
110
   * @throws IOException Could not read the R startup script.
100
   * @param key The property key name to obtain its value.
101
   * @param value The new value to set.
111102
   */
112
  public void saveRStartupScript( final String script ) throws IOException;
103
  public void putSetting( String key, String value );
113104
}
114105
M src/main/java/com/scrivenvar/service/impl/DefaultOptions.java
3131
import static com.scrivenvar.Constants.PREFS_STATE;
3232
import com.scrivenvar.service.Options;
33
import java.util.prefs.BackingStoreException;
3334
import java.util.prefs.Preferences;
3435
import static java.util.prefs.Preferences.userRoot;
...
4748
  }
4849
50
  /**
51
   * This will throw IllegalArgumentException if the value exceeds the maximum
52
   * preferences value length.
53
   *
54
   * @param key The name of the key to associate with the value.
55
   * @param value The value to persist.
56
   *
57
   * @throws BackingStoreException New value not persisted.
58
   */
4959
  @Override
50
  public void put( final String key, final String value ) {
51
    getPreferences().put( key, value );
60
  public void put( final String key, final String value )
61
    throws BackingStoreException {
62
    getState().put( key, value );
63
    getState().flush();
5264
  }
5365
5466
  @Override
55
  public String get( final String key, final String defalutValue ) {
56
    return getPreferences().get( key, defalutValue );
67
  public String get( final String key, final String value ) {
68
    return getState().get( key, value );
69
  }
70
71
  @Override
72
  public String get( final String key ) {
73
    return get( key, "" );
5774
  }
5875
M src/main/java/com/scrivenvar/service/impl/DefaultSettings.java
2828
package com.scrivenvar.service.impl;
2929
30
import static com.scrivenvar.Constants.FILE_R_STARTUP;
3130
import static com.scrivenvar.Constants.SETTINGS_NAME;
32
import com.scrivenvar.processors.InlineRProcessor;
3331
import com.scrivenvar.service.Settings;
34
import java.io.ByteArrayOutputStream;
3532
import java.io.IOException;
36
import java.io.InputStream;
3733
import java.io.InputStreamReader;
3834
import java.io.Reader;
3935
import java.net.URISyntaxException;
4036
import java.net.URL;
4137
import java.nio.charset.Charset;
42
import static java.nio.charset.StandardCharsets.UTF_8;
4338
import java.util.Iterator;
4439
import java.util.List;
...
5449
 */
5550
public class DefaultSettings implements Settings {
51
5652
  private static final char VALUE_SEPARATOR = ',';
5753
...
8783
  public int getSetting( final String property, final int defaultValue ) {
8884
    return getSettings().getInt( property, defaultValue );
85
  }
86
87
  /**
88
   * Changes key's value. This will clear the old value before setting the new
89
   * value so that the old value is erased, not changed into a list.
90
   *
91
   * @param key The property key name to obtain its value.
92
   * @param value The new value to set.
93
   */
94
  @Override
95
  public void putSetting( final String key, final String value ) {
96
    System.out.println( "PUT: " + key + " = " + value );
97
    getSettings().clearProperty( key );
98
    getSettings().addProperty( key, value );
8999
  }
90100
...
145155
    return configuration;
146156
  }
147
  
157
148158
  protected Charset getDefaultEncoding() {
149159
    return Charset.defaultCharset();
150160
  }
151
  
161
152162
  protected ListDelimiterHandler createListDelimiterHandler() {
153163
    return new DefaultListDelimiterHandler( VALUE_SEPARATOR );
...
168178
  private PropertiesConfiguration getSettings() {
169179
    return this.properties;
170
  }
171
  
172
  /**
173
   * 
174
   * @param script Script to write file back to the settings.
175
   * @throws IOException Couldn't write the string back to the file.
176
   */
177
  @Override
178
  public void saveRStartupScript( final String script ) throws IOException {
179
    assert script != null;
180
    
181
    System.out.println( "Save resource: " + script );
182
  }
183
184
  /**
185
   * Reads the R startup script into a string, or the empty string if the file
186
   * could not be read (or found). The R startup file must be UTF-8.
187
   *
188
   * @return The string content for the R startup script, or empty if not found.
189
   * @throws IOException Could not read the R startup script.
190
   */
191
  @Override
192
  public String loadRStartupScript() throws IOException {
193
    try( final InputStream in = openResource( FILE_R_STARTUP ) ) {
194
      return readFully( in );
195
    }
196
  }
197
198
  /**
199
   * Opens a resource such that it can be closed using a try/finally block.
200
   *
201
   * @param path Path to the resource to open.
202
   *
203
   * @return An open input stream ready to be read.
204
   */
205
  private InputStream openResource( final String path ) {
206
    return InlineRProcessor.class.getResourceAsStream( path );
207
  }
208
209
  private String readFully( final InputStream inputStream ) throws IOException {
210
    final byte[] buffer = new byte[ 8192 ];
211
    final ByteArrayOutputStream result = new ByteArrayOutputStream();
212
213
    int length;
214
215
    while( (length = inputStream.read( buffer )) != -1 ) {
216
      result.write( buffer, 0, length );
217
    }
218
219
    return result.toString( UTF_8.name() );
220180
  }
221
222181
}
223182
M src/main/resources/com/scrivenvar/messages.properties
11
#
2
# Copyright 2016 Karl Tauber and White Magic Software, Ltd.
2
# Copyright 2017 Karl Tauber and White Magic Software, Ltd.
33
#
44
# All rights reserved.
...
108108
Definition.menu.add=Add
109109
Definition.menu.remove=Delete
110
111
# ########################################################################
112
#
113
# About Dialog
114
#
115
# ########################################################################
116
117
Dialog.about.title=About
118
Dialog.about.header=${Main.title}
119
Dialog.about.content=Copyright 2016 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber
120110
121111
# ########################################################################
...
191181
# ########################################################################
192182
193
ImageDialog.title=Image
194
ImageDialog.chooser.imagesFilter=Images
195
ImageDialog.previewLabel.text=Markdown Preview\:
196
ImageDialog.textLabel.text=Alternate Text\:
197
ImageDialog.titleLabel.text=Title (tooltip)\:
198
ImageDialog.urlLabel.text=Image URL\:
183
Dialog.image.title=Image
184
Dialog.image.chooser.imagesFilter=Images
185
Dialog.image.previewLabel.text=Markdown Preview\:
186
Dialog.image.textLabel.text=Alternate Text\:
187
Dialog.image.titleLabel.text=Title (tooltip)\:
188
Dialog.image.urlLabel.text=Image URL\:
199189
200190
# ########################################################################
201191
#
202192
# Hyperlink
203193
#
204194
# ########################################################################
205195
206
LinkDialog.title=Link
207
LinkDialog.previewLabel.text=Markdown Preview\:
208
LinkDialog.textLabel.text=Link Text\:
209
LinkDialog.titleLabel.text=Title (tooltip)\:
210
LinkDialog.urlLabel.text=Link URL\:
196
Dialog.link.title=Link
197
Dialog.link.previewLabel.text=Markdown Preview\:
198
Dialog.link.textLabel.text=Link Text\:
199
Dialog.link.titleLabel.text=Title (tooltip)\:
200
Dialog.link.urlLabel.text=Link URL\:
201
202
# ########################################################################
203
#
204
# About
205
#
206
# ########################################################################
207
208
Dialog.about.title=About
209
Dialog.about.header=${Main.title}
210
Dialog.about.content=Copyright 2017 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber
211
212
# ########################################################################
213
#
214
# R Script
215
#
216
# ########################################################################
217
218
Dialog.rScript.title=R Startup Script
219
Dialog.rScript.content=Provide R statements to run prior to interpreting R statements embedded in the document.
211220
212221
# Options ################################################################
D src/main/resources/com/scrivenvar/startup.R
11