Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M build.gradle
1
version = '1.0.9'
1
version = '1.0.10'
22
33
apply plugin: 'java'
...
2121
dependencies {
2222
  compile 'org.controlsfx:controlsfx:8.40.12'
23
  compile 'org.fxmisc.richtext:richtextfx:0.7-M2'
23
  compile 'org.fxmisc.richtext:richtextfx:0.7-M3'
2424
  compile 'com.miglayout:miglayout-javafx:5.0'
2525
  compile 'de.jensd:fontawesomefx-fontawesome:4.5.0'
...
5555
  manifest {
5656
    attributes 'Main-Class': mainClassName
57
    attributes 'Class-Path': configurations.compile.collect { 'libs/' + it.getName() }.join(' ')
57
    attributes 'Class-Path': configurations.compile.collect {
58
     'libs/' + it.getName()
59
    }.join(' ')
5860
  }
5961
}
M src/main/java/com/scrivenvar/Constants.java
5656
  public static final String APP_TITLE = get( "application.title" );
5757
  public static final String APP_BUNDLE_NAME = get( "application.messages" );
58
  // Prevent double events when updating files on Linux (save and timestamp).
5859
  public static final int APP_WATCHDOG_TIMEOUT = get( "application.watchdog.timeout", 100 );
5960
...
8687
  public static final String DEFINITION_PROTOCOL_UNKNOWN = "unknown";
8788
  public static final String DEFINITION_PROTOCOL_FILE = "file";
89
  
90
  // Takes two parameters: line number and column number.
91
  public static final String STATUS_BAR_LINE = "Main.statusbar.line";
92
  // "OK" text
93
  public static final String STATUS_BAR_DEFAULT = "Main.statusbar.state.default";
8894
}
8995
M src/main/java/com/scrivenvar/FileEditorTab.java
5151
import javafx.stage.Window;
5252
import org.fxmisc.richtext.StyleClassedTextArea;
53
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
54
import org.fxmisc.richtext.model.TwoDimensional.Position;
5355
import org.fxmisc.undo.UndoManager;
5456
import org.fxmisc.wellbehaved.event.EventPattern;
...
6365
public final class FileEditorTab extends Tab {
6466
65
  private final Notifier alertService = Services.load(Notifier.class );
67
  private final Notifier alertService = Services.load( Notifier.class );
6668
  private EditorPane editorPane;
6769
...
185187
  public int getCaretPosition() {
186188
    return getEditor().getCaretPosition();
189
  }
190
191
  /**
192
   * Returns the caret's current row and column position.
193
   * 
194
   * @return The caret's offset into the document.
195
   */
196
  public Position getCaretOffset() {
197
    return getEditor().offsetToPosition( getCaretPosition(), Forward );
187198
  }
188199
M src/main/java/com/scrivenvar/MainWindow.java
8585
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
8686
import org.controlsfx.control.StatusBar;
87
88
/**
89
 * Main window containing a tab pane in the center for file editors.
90
 *
91
 * @author Karl Tauber and White Magic Software, Ltd.
92
 */
93
public class MainWindow implements Observer {
94
95
  private final Options options = Services.load( Options.class );
96
  private final Snitch snitch = Services.load( Snitch.class );
97
  private final Notifier notifier = Services.load( Notifier.class );
98
99
  private Scene scene;
100
  private MenuBar menuBar;
101
  private StatusBar statusBar;
102
103
  private DefinitionSource definitionSource;
104
  private DefinitionPane definitionPane;
105
  private FileEditorTabPane fileEditorPane;
106
  private HTMLPreviewPane previewPane;
107
108
  /**
109
   * Prevent re-instantiation processing classes.
110
   */
111
  private Map<FileEditorTab, Processor<String>> processors;
112
113
  public MainWindow() {
114
    initLayout();
115
    initDefinitionListener();
116
    initTabAddedListener();
117
    initTabChangedListener();
118
    initPreferences();
119
    initSnitch();
120
  }
121
122
  /**
123
   * Listen for file editor tab pane to receive an open definition source event.
124
   */
125
  private void initDefinitionListener() {
126
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
127
      (ObservableValue<? extends Path> definitionFile,
128
        final Path oldPath, final Path newPath) -> {
129
        openDefinition( newPath );
130
131
        // Indirectly refresh the resolved map.
132
        setProcessors( null );
133
134
        // Will create new processors and therefore a new resolved map.
135
        refreshSelectedTab( getActiveFileEditor() );
136
137
        updateDefinitionPane();
138
      }
139
    );
140
  }
141
142
  /**
143
   * When tabs are added, hook the various change listeners onto the new tab so
144
   * that the preview pane refreshes as necessary.
145
   */
146
  private void initTabAddedListener() {
147
    final FileEditorTabPane editorPane = getFileEditorPane();
148
149
    // Make sure the text processor kicks off when new files are opened.
150
    final ObservableList<Tab> tabs = editorPane.getTabs();
151
152
    // Update the preview pane on tab changes.
153
    tabs.addListener(
154
      (final Change<? extends Tab> change) -> {
155
        while( change.next() ) {
156
          if( change.wasAdded() ) {
157
            // Multiple tabs can be added simultaneously.
158
            for( final Tab newTab : change.getAddedSubList() ) {
159
              final FileEditorTab tab = (FileEditorTab)newTab;
160
161
              initTextChangeListener( tab );
162
              initCaretParagraphListener( tab );
163
              initVariableNameInjector( tab );
164
//              initSyntaxListener( tab );
165
            }
166
          }
167
        }
168
      }
169
    );
170
  }
171
172
  /**
173
   * Reloads the preferences from the previous load.
174
   */
175
  private void initPreferences() {
176
    restoreDefinitionSource();
177
    getFileEditorPane().restorePreferences();
178
    updateDefinitionPane();
179
  }
180
181
  /**
182
   * Listen for new tab selection events.
183
   */
184
  private void initTabChangedListener() {
185
    final FileEditorTabPane editorPane = getFileEditorPane();
186
187
    // Update the preview pane changing tabs.
188
    editorPane.addTabSelectionListener(
189
      (ObservableValue<? extends Tab> tabPane,
190
        final Tab oldTab, final Tab newTab) -> {
191
192
        // If there was no old tab, then this is a first time load, which
193
        // can be ignored.
194
        if( oldTab != null ) {
195
          if( newTab == null ) {
196
            closeRemainingTab();
197
          }
198
          else {
199
            // Update the preview with the edited text.
200
            refreshSelectedTab( (FileEditorTab)newTab );
201
          }
202
        }
203
      }
204
    );
205
  }
206
207
  private void initTextChangeListener( final FileEditorTab tab ) {
208
    tab.addTextChangeListener(
209
      (ObservableValue<? extends String> editor,
210
        final String oldValue, final String newValue) -> {
211
        refreshSelectedTab( tab );
212
      }
213
    );
214
  }
215
216
  private void initCaretParagraphListener( final FileEditorTab tab ) {
217
    tab.addCaretParagraphListener(
218
      (ObservableValue<? extends Integer> editor,
219
        final Integer oldValue, final Integer newValue) -> {
220
        refreshSelectedTab( tab );
221
      }
222
    );
223
  }
224
225
  private void initVariableNameInjector( final FileEditorTab tab ) {
226
    VariableNameInjector.listen( tab, getDefinitionPane() );
227
  }
228
229
  /**
230
   * Watch for changes to external files. In particular, this awaits
231
   * modifications to any XSL files associated with XML files being edited. When
232
   * an XSL file is modified (external to the application), the snitch's ears
233
   * perk up and the file is reloaded. This keeps the XSL transformation up to
234
   * date with what's on the file system.
235
   */
236
  private void initSnitch() {
237
    getSnitch().addObserver( this );
238
  }
239
240
  /**
241
   * Called whenever the preview pane becomes out of sync with the file editor
242
   * tab. This can be called when the text changes, the caret paragraph changes,
243
   * or the file tab changes.
244
   *
245
   * @param tab The file editor tab that has been changed in some fashion.
246
   */
247
  private void refreshSelectedTab( final FileEditorTab tab ) {
248
    if( tab.isFileOpen() ) {
249
      getPreviewPane().setPath( tab.getPath() );
250
251
      Processor<String> processor = getProcessors().get( tab );
252
253
      if( processor == null ) {
254
        processor = createProcessor( tab );
255
        getProcessors().put( tab, processor );
256
      }
257
258
      try {
259
        processor.processChain( tab.getEditorText() );
260
        getNotifier().clear();
261
      } catch( final Exception ex ) {
262
        error( ex );
263
      }
264
    }
265
  }
266
267
  /**
268
   * Returns the variable map of interpolated definitions.
269
   *
270
   * @return A map to help dereference variables.
271
   */
272
  private Map<String, String> getResolvedMap() {
273
    return getDefinitionSource().getResolvedMap();
274
  }
275
276
  /**
277
   * Returns the root node for the hierarchical definition source.
278
   *
279
   * @return Data to display in the definition pane.
280
   */
281
  private TreeView<String> getTreeView() {
282
    try {
283
      return getDefinitionSource().asTreeView();
284
    } catch( Exception e ) {
285
      error( e );
286
    }
287
288
    return new TreeView<>();
289
  }
290
291
  /**
292
   * Called when a definition source is opened.
293
   *
294
   * @param path Path to the definition source that was opened.
295
   */
296
  private void openDefinition( final Path path ) {
297
    try {
298
      final DefinitionSource ds = createDefinitionSource( path.toString() );
299
      setDefinitionSource( ds );
300
      storeDefinitionSource();
301
      updateDefinitionPane();
302
    } catch( final Exception e ) {
303
      error( e );
304
    }
305
  }
306
307
  private void updateDefinitionPane() {
308
    getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
309
  }
310
311
  private void restoreDefinitionSource() {
312
    final Preferences preferences = getPreferences();
313
    final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
314
315
    // If there's no definition source set, don't try to load it.
316
    if( source != null ) {
317
      setDefinitionSource( createDefinitionSource( source ) );
318
    }
319
  }
320
321
  private void storeDefinitionSource() {
322
    final Preferences preferences = getPreferences();
323
    final DefinitionSource ds = getDefinitionSource();
324
325
    preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
326
  }
327
328
  /**
329
   * Called when the last open tab is closed to clear the preview pane.
330
   */
331
  private void closeRemainingTab() {
332
    getPreviewPane().clear();
333
  }
334
335
  /**
336
   * Called when an exception occurs that warrants the user's attention.
337
   *
338
   * @param e The exception with a message that the user should know about.
339
   */
340
  private void error( final Exception e ) {
341
    getNotifier().notify( e );
342
  }
343
344
  //---- File actions -------------------------------------------------------
345
  /**
346
   * Called when an observable instance has changed. This includes the snitch
347
   * service and the notify service.
348
   *
349
   * @param observable The observed instance.
350
   * @param value The noteworthy item.
351
   */
352
  @Override
353
  public void update( final Observable observable, final Object value ) {
354
    if( value != null ) {
355
      if( observable instanceof Snitch && value instanceof Path ) {
356
        update( (Path)value );
357
      }
358
      else if( observable instanceof Notifier && value instanceof String ) {
359
        final String s = (String)value;
360
        final int index = s.indexOf( '\n' );
361
        final String message = s.substring( 0, index > 0 ? index : s.length() );
362
363
        getStatusBar().setText( message );
364
      }
365
    }
366
  }
367
368
  /**
369
   * Called when a file has been modified.
370
   *
371
   * @param file Path to the modified file.
372
   */
373
  private void update( final Path file ) {
374
    // Avoid throwing IllegalStateException by running from a non-JavaFX thread.
375
    Platform.runLater(
376
      () -> {
377
        // Brute-force XSLT file reload by re-instantiating all processors.
378
        resetProcessors();
379
        refreshSelectedTab( getActiveFileEditor() );
380
      }
381
    );
382
  }
383
384
  /**
385
   * After resetting the processors, they will refresh anew to be up-to-date
386
   * with the files (text and definition) currently loaded into the editor.
387
   */
388
  private void resetProcessors() {
389
    getProcessors().clear();
390
  }
391
392
  //---- File actions -------------------------------------------------------
393
  private void fileNew() {
394
    getFileEditorPane().newEditor();
395
  }
396
397
  private void fileOpen() {
398
    getFileEditorPane().openFileDialog();
399
  }
400
401
  private void fileClose() {
402
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
403
  }
404
405
  private void fileCloseAll() {
406
    getFileEditorPane().closeAllEditors();
407
  }
408
409
  private void fileSave() {
410
    getFileEditorPane().saveEditor( getActiveFileEditor() );
411
  }
412
413
  private void fileSaveAll() {
414
    getFileEditorPane().saveAllEditors();
415
  }
416
417
  private void fileExit() {
418
    final Window window = getWindow();
419
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
420
  }
421
422
  //---- Help actions -------------------------------------------------------
423
  private void helpAbout() {
424
    Alert alert = new Alert( AlertType.INFORMATION );
425
    alert.setTitle( get( "Dialog.about.title" ) );
426
    alert.setHeaderText( get( "Dialog.about.header" ) );
427
    alert.setContentText( get( "Dialog.about.content" ) );
428
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
429
    alert.initOwner( getWindow() );
430
431
    alert.showAndWait();
432
  }
433
434
  //---- Convenience accessors ----------------------------------------------
435
  private float getFloat( final String key, final float defaultValue ) {
436
    return getPreferences().getFloat( key, defaultValue );
437
  }
438
439
  private Preferences getPreferences() {
440
    return getOptions().getState();
441
  }
442
443
  protected Scene getScene() {
444
    if( this.scene == null ) {
445
      this.scene = createScene();
446
    }
447
448
    return this.scene;
449
  }
450
451
  public Window getWindow() {
452
    return getScene().getWindow();
453
  }
454
455
  private MarkdownEditorPane getActiveEditor() {
456
    final EditorPane pane = getActiveFileEditor().getEditorPane();
457
458
    return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
459
  }
460
461
  private FileEditorTab getActiveFileEditor() {
462
    return getFileEditorPane().getActiveFileEditor();
463
  }
464
465
  //---- Member accessors ---------------------------------------------------
466
  private void setScene( Scene scene ) {
467
    this.scene = scene;
468
  }
469
470
  private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) {
471
    this.processors = map;
472
  }
473
474
  private Map<FileEditorTab, Processor<String>> getProcessors() {
475
    if( this.processors == null ) {
476
      setProcessors( new HashMap<>() );
477
    }
478
479
    return this.processors;
480
  }
481
482
  private FileEditorTabPane getFileEditorPane() {
483
    if( this.fileEditorPane == null ) {
484
      this.fileEditorPane = createFileEditorPane();
485
    }
486
487
    return this.fileEditorPane;
488
  }
489
490
  private HTMLPreviewPane getPreviewPane() {
491
    if( this.previewPane == null ) {
492
      this.previewPane = createPreviewPane();
493
    }
494
495
    return this.previewPane;
496
  }
497
498
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
499
    this.definitionSource = definitionSource;
500
  }
501
502
  private DefinitionSource getDefinitionSource() {
503
    if( this.definitionSource == null ) {
504
      this.definitionSource = new EmptyDefinitionSource();
505
    }
506
507
    return this.definitionSource;
508
  }
509
510
  private DefinitionPane getDefinitionPane() {
511
    if( this.definitionPane == null ) {
512
      this.definitionPane = createDefinitionPane();
513
    }
514
515
    return this.definitionPane;
516
  }
517
518
  private Options getOptions() {
519
    return this.options;
520
  }
521
522
  private Snitch getSnitch() {
523
    return this.snitch;
524
  }
525
526
  private Notifier getNotifier() {
527
    return this.notifier;
528
  }
529
530
  public void setMenuBar( final MenuBar menuBar ) {
531
    this.menuBar = menuBar;
532
  }
533
534
  public MenuBar getMenuBar() {
535
    return this.menuBar;
536
  }
537
538
  private synchronized StatusBar getStatusBar() {
539
    if( this.statusBar == null ) {
540
      this.statusBar = createStatusBar();
541
    }
542
543
    return this.statusBar;
544
  }
545
546
  //---- Member creators ----------------------------------------------------
547
  /**
548
   * Factory to create processors that are suited to different file types.
549
   *
550
   * @param tab The tab that is subjected to processing.
551
   *
552
   * @return A processor suited to the file type specified by the tab's path.
553
   */
554
  private Processor<String> createProcessor( final FileEditorTab tab ) {
555
    return createProcessorFactory().createProcessor( tab );
556
  }
557
558
  private ProcessorFactory createProcessorFactory() {
559
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
560
  }
561
562
  private DefinitionSource createDefinitionSource( final String path ) {
563
    return createDefinitionFactory().createDefinitionSource( path );
564
  }
565
566
  /**
567
   * Create an editor pane to hold file editor tabs.
568
   *
569
   * @return A new instance, never null.
570
   */
571
  private FileEditorTabPane createFileEditorPane() {
572
    return new FileEditorTabPane();
573
  }
574
575
  private HTMLPreviewPane createPreviewPane() {
576
    return new HTMLPreviewPane();
577
  }
578
579
  private DefinitionPane createDefinitionPane() {
580
    return new DefinitionPane( getTreeView() );
581
  }
582
583
  private DefinitionFactory createDefinitionFactory() {
584
    return new DefinitionFactory();
585
  }
586
587
  private StatusBar createStatusBar() {
588
    return new StatusBar();
589
  }
590
591
  private Scene createScene() {
592
    final SplitPane splitPane = new SplitPane(
593
      getDefinitionPane().getNode(),
594
      getFileEditorPane().getNode(),
595
      getPreviewPane().getNode() );
596
597
    splitPane.setDividerPositions(
598
      getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
599
      getFloat( K_PANE_SPLIT_EDITOR, .45f ),
600
      getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
601
602
    // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
603
    final BorderPane borderPane = new BorderPane();
604
    borderPane.setPrefSize( 1024, 800 );
605
    borderPane.setTop( createMenuBar() );
606
    borderPane.setBottom( getStatusBar() );
607
    borderPane.setCenter( splitPane );
608
609
    final VBox box = new VBox();
610
    box.setAlignment( Pos.BASELINE_CENTER );
611
    box.getChildren().add( new Text( "Line %d of %d" ) );
612
    getStatusBar().getRightItems().add( box );
613
614
    return new Scene( borderPane );
87
import org.fxmisc.richtext.model.TwoDimensional.Position;
88
89
/**
90
 * Main window containing a tab pane in the center for file editors.
91
 *
92
 * @author Karl Tauber and White Magic Software, Ltd.
93
 */
94
public class MainWindow implements Observer {
95
96
  private final Options options = Services.load( Options.class );
97
  private final Snitch snitch = Services.load( Snitch.class );
98
  private final Notifier notifier = Services.load( Notifier.class );
99
100
  private Scene scene;
101
  private MenuBar menuBar;
102
  private StatusBar statusBar;
103
  private Text lineNumberText;
104
105
  private DefinitionSource definitionSource;
106
  private DefinitionPane definitionPane;
107
  private FileEditorTabPane fileEditorPane;
108
  private HTMLPreviewPane previewPane;
109
110
  /**
111
   * Prevent re-instantiation processing classes.
112
   */
113
  private Map<FileEditorTab, Processor<String>> processors;
114
115
  public MainWindow() {
116
    initLayout();
117
    initDefinitionListener();
118
    initTabAddedListener();
119
    initTabChangedListener();
120
    initPreferences();
121
    initSnitch();
122
  }
123
124
  /**
125
   * Listen for file editor tab pane to receive an open definition source event.
126
   */
127
  private void initDefinitionListener() {
128
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
129
      (ObservableValue<? extends Path> definitionFile,
130
        final Path oldPath, final Path newPath) -> {
131
        openDefinition( newPath );
132
133
        // Indirectly refresh the resolved map.
134
        setProcessors( null );
135
136
        // Will create new processors and therefore a new resolved map.
137
        refreshSelectedTab( getActiveFileEditor() );
138
        updateDefinitionPane();
139
      }
140
    );
141
  }
142
143
  /**
144
   * When tabs are added, hook the various change listeners onto the new tab so
145
   * that the preview pane refreshes as necessary.
146
   */
147
  private void initTabAddedListener() {
148
    final FileEditorTabPane editorPane = getFileEditorPane();
149
150
    // Make sure the text processor kicks off when new files are opened.
151
    final ObservableList<Tab> tabs = editorPane.getTabs();
152
153
    // Update the preview pane on tab changes.
154
    tabs.addListener(
155
      (final Change<? extends Tab> change) -> {
156
        while( change.next() ) {
157
          if( change.wasAdded() ) {
158
            // Multiple tabs can be added simultaneously.
159
            for( final Tab newTab : change.getAddedSubList() ) {
160
              final FileEditorTab tab = (FileEditorTab)newTab;
161
162
              initTextChangeListener( tab );
163
              initCaretParagraphListener( tab );
164
              initVariableNameInjector( tab );
165
//              initSyntaxListener( tab );
166
            }
167
          }
168
        }
169
      }
170
    );
171
  }
172
173
  /**
174
   * Reloads the preferences from the previous load.
175
   */
176
  private void initPreferences() {
177
    restoreDefinitionSource();
178
    getFileEditorPane().restorePreferences();
179
    updateDefinitionPane();
180
  }
181
182
  /**
183
   * Listen for new tab selection events.
184
   */
185
  private void initTabChangedListener() {
186
    final FileEditorTabPane editorPane = getFileEditorPane();
187
188
    // Update the preview pane changing tabs.
189
    editorPane.addTabSelectionListener(
190
      (ObservableValue<? extends Tab> tabPane,
191
        final Tab oldTab, final Tab newTab) -> {
192
193
        // If there was no old tab, then this is a first time load, which
194
        // can be ignored.
195
        if( oldTab != null ) {
196
          if( newTab == null ) {
197
            closeRemainingTab();
198
          }
199
          else {
200
            // Update the preview with the edited text.
201
            refreshSelectedTab( (FileEditorTab)newTab );
202
          }
203
        }
204
      }
205
    );
206
  }
207
208
  private void initTextChangeListener( final FileEditorTab tab ) {
209
    tab.addTextChangeListener(
210
      (ObservableValue<? extends String> editor,
211
        final String oldValue, final String newValue) -> {
212
        refreshSelectedTab( tab );
213
      }
214
    );
215
  }
216
217
  private void initCaretParagraphListener( final FileEditorTab tab ) {
218
    tab.addCaretParagraphListener(
219
      (ObservableValue<? extends Integer> editor,
220
        final Integer oldValue, final Integer newValue) -> {
221
        refreshSelectedTab( tab );
222
      }
223
    );
224
  }
225
226
  private void initVariableNameInjector( final FileEditorTab tab ) {
227
    VariableNameInjector.listen( tab, getDefinitionPane() );
228
  }
229
230
  /**
231
   * Watch for changes to external files. In particular, this awaits
232
   * modifications to any XSL files associated with XML files being edited. When
233
   * an XSL file is modified (external to the application), the snitch's ears
234
   * perk up and the file is reloaded. This keeps the XSL transformation up to
235
   * date with what's on the file system.
236
   */
237
  private void initSnitch() {
238
    getSnitch().addObserver( this );
239
  }
240
241
  /**
242
   * Called whenever the preview pane becomes out of sync with the file editor
243
   * tab. This can be called when the text changes, the caret paragraph changes,
244
   * or the file tab changes.
245
   *
246
   * @param tab The file editor tab that has been changed in some fashion.
247
   */
248
  private void refreshSelectedTab( final FileEditorTab tab ) {
249
    if( tab.isFileOpen() ) {
250
      getPreviewPane().setPath( tab.getPath() );
251
252
      // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
253
      final Position p = tab.getCaretOffset();
254
      getLineNumberText().setText(
255
        get( STATUS_BAR_LINE, p.getMajor() + 1, p.getMinor() + 1 )
256
      );
257
258
      Processor<String> processor = getProcessors().get( tab );
259
260
      if( processor == null ) {
261
        processor = createProcessor( tab );
262
        getProcessors().put( tab, processor );
263
      }
264
265
      try {
266
        processor.processChain( tab.getEditorText() );
267
        getNotifier().clear();
268
      } catch( final Exception ex ) {
269
        error( ex );
270
      }
271
    }
272
  }
273
274
  /**
275
   * Returns the variable map of interpolated definitions.
276
   *
277
   * @return A map to help dereference variables.
278
   */
279
  private Map<String, String> getResolvedMap() {
280
    return getDefinitionSource().getResolvedMap();
281
  }
282
283
  /**
284
   * Returns the root node for the hierarchical definition source.
285
   *
286
   * @return Data to display in the definition pane.
287
   */
288
  private TreeView<String> getTreeView() {
289
    try {
290
      return getDefinitionSource().asTreeView();
291
    } catch( Exception e ) {
292
      error( e );
293
    }
294
295
    return new TreeView<>();
296
  }
297
298
  /**
299
   * Called when a definition source is opened.
300
   *
301
   * @param path Path to the definition source that was opened.
302
   */
303
  private void openDefinition( final Path path ) {
304
    try {
305
      final DefinitionSource ds = createDefinitionSource( path.toString() );
306
      setDefinitionSource( ds );
307
      storeDefinitionSource();
308
      updateDefinitionPane();
309
    } catch( final Exception e ) {
310
      error( e );
311
    }
312
  }
313
314
  private void updateDefinitionPane() {
315
    getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
316
  }
317
318
  private void restoreDefinitionSource() {
319
    final Preferences preferences = getPreferences();
320
    final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
321
322
    // If there's no definition source set, don't try to load it.
323
    if( source != null ) {
324
      setDefinitionSource( createDefinitionSource( source ) );
325
    }
326
  }
327
328
  private void storeDefinitionSource() {
329
    final Preferences preferences = getPreferences();
330
    final DefinitionSource ds = getDefinitionSource();
331
332
    preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
333
  }
334
335
  /**
336
   * Called when the last open tab is closed to clear the preview pane.
337
   */
338
  private void closeRemainingTab() {
339
    getPreviewPane().clear();
340
  }
341
342
  /**
343
   * Called when an exception occurs that warrants the user's attention.
344
   *
345
   * @param e The exception with a message that the user should know about.
346
   */
347
  private void error( final Exception e ) {
348
    getNotifier().notify( e );
349
  }
350
351
  //---- File actions -------------------------------------------------------
352
  /**
353
   * Called when an observable instance has changed. This includes the snitch
354
   * service and the notify service.
355
   *
356
   * @param observable The observed instance.
357
   * @param value The noteworthy item.
358
   */
359
  @Override
360
  public void update( final Observable observable, final Object value ) {
361
    if( value != null ) {
362
      if( observable instanceof Snitch && value instanceof Path ) {
363
        update( (Path)value );
364
      }
365
      else if( observable instanceof Notifier && value instanceof String ) {
366
        final String s = (String)value;
367
        final int index = s.indexOf( '\n' );
368
        final String message = s.substring( 0, index > 0 ? index : s.length() );
369
370
        getStatusBar().setText( message );
371
      }
372
    }
373
  }
374
375
  /**
376
   * Called when a file has been modified.
377
   *
378
   * @param file Path to the modified file.
379
   */
380
  private void update( final Path file ) {
381
    // Avoid throwing IllegalStateException by running from a non-JavaFX thread.
382
    Platform.runLater(
383
      () -> {
384
        // Brute-force XSLT file reload by re-instantiating all processors.
385
        resetProcessors();
386
        refreshSelectedTab( getActiveFileEditor() );
387
      }
388
    );
389
  }
390
391
  /**
392
   * After resetting the processors, they will refresh anew to be up-to-date
393
   * with the files (text and definition) currently loaded into the editor.
394
   */
395
  private void resetProcessors() {
396
    getProcessors().clear();
397
  }
398
399
  //---- File actions -------------------------------------------------------
400
  private void fileNew() {
401
    getFileEditorPane().newEditor();
402
  }
403
404
  private void fileOpen() {
405
    getFileEditorPane().openFileDialog();
406
  }
407
408
  private void fileClose() {
409
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
410
  }
411
412
  private void fileCloseAll() {
413
    getFileEditorPane().closeAllEditors();
414
  }
415
416
  private void fileSave() {
417
    getFileEditorPane().saveEditor( getActiveFileEditor() );
418
  }
419
420
  private void fileSaveAll() {
421
    getFileEditorPane().saveAllEditors();
422
  }
423
424
  private void fileExit() {
425
    final Window window = getWindow();
426
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
427
  }
428
429
  //---- Help actions -------------------------------------------------------
430
  private void helpAbout() {
431
    Alert alert = new Alert( AlertType.INFORMATION );
432
    alert.setTitle( get( "Dialog.about.title" ) );
433
    alert.setHeaderText( get( "Dialog.about.header" ) );
434
    alert.setContentText( get( "Dialog.about.content" ) );
435
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
436
    alert.initOwner( getWindow() );
437
438
    alert.showAndWait();
439
  }
440
441
  //---- Convenience accessors ----------------------------------------------
442
  private float getFloat( final String key, final float defaultValue ) {
443
    return getPreferences().getFloat( key, defaultValue );
444
  }
445
446
  private Preferences getPreferences() {
447
    return getOptions().getState();
448
  }
449
450
  protected Scene getScene() {
451
    if( this.scene == null ) {
452
      this.scene = createScene();
453
    }
454
455
    return this.scene;
456
  }
457
458
  public Window getWindow() {
459
    return getScene().getWindow();
460
  }
461
462
  private MarkdownEditorPane getActiveEditor() {
463
    final EditorPane pane = getActiveFileEditor().getEditorPane();
464
465
    return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
466
  }
467
468
  private FileEditorTab getActiveFileEditor() {
469
    return getFileEditorPane().getActiveFileEditor();
470
  }
471
472
  //---- Member accessors ---------------------------------------------------
473
  private void setScene( Scene scene ) {
474
    this.scene = scene;
475
  }
476
477
  private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) {
478
    this.processors = map;
479
  }
480
481
  private Map<FileEditorTab, Processor<String>> getProcessors() {
482
    if( this.processors == null ) {
483
      setProcessors( new HashMap<>() );
484
    }
485
486
    return this.processors;
487
  }
488
489
  private FileEditorTabPane getFileEditorPane() {
490
    if( this.fileEditorPane == null ) {
491
      this.fileEditorPane = createFileEditorPane();
492
    }
493
494
    return this.fileEditorPane;
495
  }
496
497
  private HTMLPreviewPane getPreviewPane() {
498
    if( this.previewPane == null ) {
499
      this.previewPane = createPreviewPane();
500
    }
501
502
    return this.previewPane;
503
  }
504
505
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
506
    this.definitionSource = definitionSource;
507
  }
508
509
  private DefinitionSource getDefinitionSource() {
510
    if( this.definitionSource == null ) {
511
      this.definitionSource = new EmptyDefinitionSource();
512
    }
513
514
    return this.definitionSource;
515
  }
516
517
  private DefinitionPane getDefinitionPane() {
518
    if( this.definitionPane == null ) {
519
      this.definitionPane = createDefinitionPane();
520
    }
521
522
    return this.definitionPane;
523
  }
524
525
  private Options getOptions() {
526
    return this.options;
527
  }
528
529
  private Snitch getSnitch() {
530
    return this.snitch;
531
  }
532
533
  private Notifier getNotifier() {
534
    return this.notifier;
535
  }
536
537
  public void setMenuBar( final MenuBar menuBar ) {
538
    this.menuBar = menuBar;
539
  }
540
541
  public MenuBar getMenuBar() {
542
    return this.menuBar;
543
  }
544
545
  private Text getLineNumberText() {
546
    if( this.lineNumberText == null ) {
547
      this.lineNumberText = createLineNumberText();
548
    }
549
550
    return this.lineNumberText;
551
  }
552
553
  private synchronized StatusBar getStatusBar() {
554
    if( this.statusBar == null ) {
555
      this.statusBar = createStatusBar();
556
    }
557
558
    return this.statusBar;
559
  }
560
561
  //---- Member creators ----------------------------------------------------
562
  /**
563
   * Factory to create processors that are suited to different file types.
564
   *
565
   * @param tab The tab that is subjected to processing.
566
   *
567
   * @return A processor suited to the file type specified by the tab's path.
568
   */
569
  private Processor<String> createProcessor( final FileEditorTab tab ) {
570
    return createProcessorFactory().createProcessor( tab );
571
  }
572
573
  private ProcessorFactory createProcessorFactory() {
574
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
575
  }
576
577
  private DefinitionSource createDefinitionSource( final String path ) {
578
    return createDefinitionFactory().createDefinitionSource( path );
579
  }
580
581
  /**
582
   * Create an editor pane to hold file editor tabs.
583
   *
584
   * @return A new instance, never null.
585
   */
586
  private FileEditorTabPane createFileEditorPane() {
587
    return new FileEditorTabPane();
588
  }
589
590
  private HTMLPreviewPane createPreviewPane() {
591
    return new HTMLPreviewPane();
592
  }
593
594
  private DefinitionPane createDefinitionPane() {
595
    return new DefinitionPane( getTreeView() );
596
  }
597
598
  private DefinitionFactory createDefinitionFactory() {
599
    return new DefinitionFactory();
600
  }
601
602
  private StatusBar createStatusBar() {
603
    return new StatusBar();
604
  }
605
606
  private Scene createScene() {
607
    final SplitPane splitPane = new SplitPane(
608
      getDefinitionPane().getNode(),
609
      getFileEditorPane().getNode(),
610
      getPreviewPane().getNode() );
611
612
    splitPane.setDividerPositions(
613
      getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
614
      getFloat( K_PANE_SPLIT_EDITOR, .45f ),
615
      getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
616
617
    // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
618
    final BorderPane borderPane = new BorderPane();
619
    borderPane.setPrefSize( 1024, 800 );
620
    borderPane.setTop( createMenuBar() );
621
    borderPane.setBottom( getStatusBar() );
622
    borderPane.setCenter( splitPane );
623
624
    final VBox box = new VBox();
625
    box.setAlignment( Pos.BASELINE_CENTER );
626
    box.getChildren().add( getLineNumberText() );
627
    getStatusBar().getRightItems().add( box );
628
629
    return new Scene( borderPane );
630
  }
631
632
  private Text createLineNumberText() {
633
    return new Text( get( STATUS_BAR_LINE, 1, 1 ) );
615634
  }
616635
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;
53
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
54
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
55
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
56
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
57
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
58
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
59
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
60
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
61
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
62
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
63
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
64
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
65
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
66
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
67
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
68
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
69
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
70
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
71
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
72
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
73
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
74
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
75
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
76
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
77
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
78
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
79
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
80
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
8150
8251
/**
...
9968
    textArea.setWrapText( true );
10069
    textArea.getStyleClass().add( "markdown-editor" );
101
    textArea.getStylesheets().add(STYLESHEET_MARKDOWN );
70
    textArea.getStylesheets().add( STYLESHEET_MARKDOWN );
10271
10372
    addEventListener( keyPressed( ENTER ), this::enterPressed );
...
12291
        // indent new line with same whitespace characters and list markers as current line
12392
        newText = newText.concat( matcher.group( 1 ) );
124
      } else {
93
      }
94
      else {
12595
        // current line contains only whitespace characters and list markers
12696
        // --> empty current line
12797
        final int caretPosition = textArea.getCaretPosition();
12898
        textArea.selectRange( caretPosition - currentLine.length(), caretPosition );
12999
      }
130100
    }
131101
132102
    textArea.replaceSelection( newText );
103
104
    // Ensure that the window scrolls when Enter is pressed at the bottom of
105
    // the pane.
106
    textArea.requestFollowCaret();
133107
  }
134108
...
194168
      if( trailingIsEmpty ) {
195169
        leading = str;
196
      } else {
170
      }
171
      else {
197172
        trailing = str;
198173
      }
M src/main/java/com/scrivenvar/processors/InlineRProcessor.java
6363
  }
6464
65
  public void init( final Path workingDirectory ) {
65
  /**
66
   * @see https://github.com/DaveJarvis/scrivenvar/issues/30
67
   * @param workingDirectory 
68
   */
69
  private void init( final Path workingDirectory ) {
6670
    // In Windows, path characters must be changed from escape chars.
6771
    eval( replace( ""
...
8892
      sb.append( text.substring( prevIndex, currIndex ) );
8993
94
      // Jump to the start of the R statement.
9095
      prevIndex = currIndex + prefixLength;
9196
...
120125
121126
    // Copy from the previous index to the end of the string.
122
    sb.append( text.substring( min( prevIndex, length ) ) );
123
124
    return sb.toString();
127
    return sb.append( text.substring( min( prevIndex, length ) ) ).toString();
125128
  }
126129
M src/main/resources/com/scrivenvar/messages.properties
9191
# ########################################################################
9292
#
93
# Status Bar
94
#
95
# ########################################################################
96
97
Main.statusbar.line=Line {0} of {1}
98
Main.statusbar.state.default=OK
99
100
# ########################################################################
101
#
93102
# About Dialog
94103
#