Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M build.gradle
1
version = '1.0.12'
1
version = '1.0.13'
22
33
apply plugin: 'java'
M src/main/java/com/scrivenvar/Constants.java
9191
  public static final String STATUS_BAR_LINE = "Main.statusbar.line";
9292
  // "OK" text
93
  public static final String STATUS_BAR_DEFAULT = get( "Main.statusbar.state.default" );
93
  public static final String STATUS_BAR_DEFAULT = Messages.get( "Main.statusbar.state.default" );
94
95
  public static final String STATUS_PARSE_ERROR = "Main.statusbar.parse.error";
9496
}
9597
M src/main/java/com/scrivenvar/Main.java
3232
import com.scrivenvar.service.Options;
3333
import com.scrivenvar.service.Snitch;
34
import com.scrivenvar.service.events.Notifier;
3435
import com.scrivenvar.util.StageState;
36
import java.util.logging.LogManager;
3537
import javafx.application.Application;
3638
import javafx.scene.Scene;
3739
import javafx.scene.image.Image;
3840
import javafx.stage.Stage;
39
import com.scrivenvar.service.events.Notifier;
4041
4142
/**
...
5556
5657
  public static void main( final String[] args ) {
58
    initLogger();
5759
    initPreferences();
5860
    launch( args );
61
  }
62
63
  /**
64
   * Prevents JavaFX from logging to standard error.
65
   *
66
   * @see http://stackoverflow.com/a/41476462/59087
67
   */
68
  private static void initLogger() {
69
    LogManager.getLogManager().reset();
5970
  }
6071
...
100111
   */
101112
  private void initNotifyService() {
102
    final Notifier service = Services.load(Notifier.class );
103
    service.addObserver( getMainWindow() );
113
    final Notifier notifier = Services.load( Notifier.class );
114
    notifier.addObserver( getMainWindow() );
104115
  }
105116
M src/main/java/com/scrivenvar/MainWindow.java
134134
        setProcessors( null );
135135
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 ) );
136
        updateDefinitionPane();
137
138
        // Will create new processors and therefore a new resolved map.
139
        refreshSelectedTab( getActiveFileEditor() );
140
      }
141
    );
142
  }
143
144
  /**
145
   * When tabs are added, hook the various change listeners onto the new tab so
146
   * that the preview pane refreshes as necessary.
147
   */
148
  private void initTabAddedListener() {
149
    final FileEditorTabPane editorPane = getFileEditorPane();
150
151
    // Make sure the text processor kicks off when new files are opened.
152
    final ObservableList<Tab> tabs = editorPane.getTabs();
153
154
    // Update the preview pane on tab changes.
155
    tabs.addListener(
156
      (final Change<? extends Tab> change) -> {
157
        while( change.next() ) {
158
          if( change.wasAdded() ) {
159
            // Multiple tabs can be added simultaneously.
160
            for( final Tab newTab : change.getAddedSubList() ) {
161
              final FileEditorTab tab = (FileEditorTab)newTab;
162
163
              initTextChangeListener( tab );
164
              initCaretParagraphListener( tab );
165
              initVariableNameInjector( tab );
166
//              initSyntaxListener( tab );
167
            }
168
          }
169
        }
170
      }
171
    );
172
  }
173
174
  /**
175
   * Reloads the preferences from the previous load.
176
   */
177
  private void initPreferences() {
178
    restoreDefinitionSource();
179
    getFileEditorPane().restorePreferences();
180
    updateDefinitionPane();
181
  }
182
183
  /**
184
   * Listen for new tab selection events.
185
   */
186
  private void initTabChangedListener() {
187
    final FileEditorTabPane editorPane = getFileEditorPane();
188
189
    // Update the preview pane changing tabs.
190
    editorPane.addTabSelectionListener(
191
      (ObservableValue<? extends Tab> tabPane,
192
        final Tab oldTab, final Tab newTab) -> {
193
194
        // If there was no old tab, then this is a first time load, which
195
        // can be ignored.
196
        if( oldTab != null ) {
197
          if( newTab == null ) {
198
            closeRemainingTab();
199
          }
200
          else {
201
            // Update the preview with the edited text.
202
            refreshSelectedTab( (FileEditorTab)newTab );
203
          }
204
        }
205
      }
206
    );
207
  }
208
209
  private void initTextChangeListener( final FileEditorTab tab ) {
210
    tab.addTextChangeListener(
211
      (ObservableValue<? extends String> editor,
212
        final String oldValue, final String newValue) -> {
213
        refreshSelectedTab( tab );
214
      }
215
    );
216
  }
217
218
  private void initCaretParagraphListener( final FileEditorTab tab ) {
219
    tab.addCaretParagraphListener(
220
      (ObservableValue<? extends Integer> editor,
221
        final Integer oldValue, final Integer newValue) -> {
222
        refreshSelectedTab( tab );
223
      }
224
    );
225
  }
226
227
  private void initVariableNameInjector( final FileEditorTab tab ) {
228
    VariableNameInjector.listen( tab, getDefinitionPane() );
229
  }
230
231
  /**
232
   * Watch for changes to external files. In particular, this awaits
233
   * modifications to any XSL files associated with XML files being edited. When
234
   * an XSL file is modified (external to the application), the snitch's ears
235
   * perk up and the file is reloaded. This keeps the XSL transformation up to
236
   * date with what's on the file system.
237
   */
238
  private void initSnitch() {
239
    getSnitch().addObserver( this );
240
  }
241
242
  /**
243
   * Called whenever the preview pane becomes out of sync with the file editor
244
   * tab. This can be called when the text changes, the caret paragraph changes,
245
   * or the file tab changes.
246
   *
247
   * @param tab The file editor tab that has been changed in some fashion.
248
   */
249
  private void refreshSelectedTab( final FileEditorTab tab ) {
250
    if( tab.isFileOpen() ) {
251
      getPreviewPane().setPath( tab.getPath() );
252
253
      // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
254
      final Position p = tab.getCaretOffset();
255
      getLineNumberText().setText(
256
        get( STATUS_BAR_LINE,
257
          p.getMajor() + 1,
258
          p.getMinor() + 1,
259
          tab.getCaretPosition() + 1
260
        )
261
      );
262
263
      Processor<String> processor = getProcessors().get( tab );
264
265
      if( processor == null ) {
266
        processor = createProcessor( tab );
267
        getProcessors().put( tab, processor );
268
      }
269
270
      try {
271
        getNotifier().clear();
272
        processor.processChain( tab.getEditorText() );
273
      } catch( final Exception ex ) {
274
        error( ex );
275
      }
276
    }
277
  }
278
279
  /**
280
   * Returns the variable map of interpolated definitions.
281
   *
282
   * @return A map to help dereference variables.
283
   */
284
  private Map<String, String> getResolvedMap() {
285
    return getDefinitionSource().getResolvedMap();
286
  }
287
288
  /**
289
   * Returns the root node for the hierarchical definition source.
290
   *
291
   * @return Data to display in the definition pane.
292
   */
293
  private TreeView<String> getTreeView() {
294
    try {
295
      return getDefinitionSource().asTreeView();
296
    } catch( Exception e ) {
297
      error( e );
298
    }
299
300
    // Slightly redundant as getDefinitionSource() might have returned an
301
    // empty definition source.
302
    return (new EmptyDefinitionSource()).asTreeView();
303
  }
304
305
  /**
306
   * Called when a definition source is opened.
307
   *
308
   * @param path Path to the definition source that was opened.
309
   */
310
  private void openDefinition( final Path path ) {
311
    try {
312
      final DefinitionSource ds = createDefinitionSource( path.toString() );
313
      setDefinitionSource( ds );
314
      storeDefinitionSource();
315
      updateDefinitionPane();
316
    } catch( final Exception e ) {
317
      error( e );
318
    }
319
  }
320
321
  private void updateDefinitionPane() {
322
    getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
323
  }
324
325
  private void restoreDefinitionSource() {
326
    final Preferences preferences = getPreferences();
327
    final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
328
329
    // If there's no definition source set, don't try to load it.
330
    if( source != null ) {
331
      setDefinitionSource( createDefinitionSource( source ) );
332
    }
333
  }
334
335
  private void storeDefinitionSource() {
336
    final Preferences preferences = getPreferences();
337
    final DefinitionSource ds = getDefinitionSource();
338
339
    preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
340
  }
341
342
  /**
343
   * Called when the last open tab is closed to clear the preview pane.
344
   */
345
  private void closeRemainingTab() {
346
    getPreviewPane().clear();
347
  }
348
349
  /**
350
   * Called when an exception occurs that warrants the user's attention.
351
   *
352
   * @param e The exception with a message that the user should know about.
353
   */
354
  private void error( final Exception e ) {
355
    getNotifier().notify( e );
356
  }
357
358
  //---- File actions -------------------------------------------------------
359
  /**
360
   * Called when an observable instance has changed. This includes the snitch
361
   * service and the notify service.
362
   *
363
   * @param observable The observed instance.
364
   * @param value The noteworthy item.
365
   */
366
  @Override
367
  public void update( final Observable observable, final Object value ) {
368
    if( value != null ) {
369
      if( observable instanceof Snitch && value instanceof Path ) {
370
        update( (Path)value );
371
      }
372
      else if( observable instanceof Notifier && value instanceof String ) {
373
        final String s = (String)value;
374
        final int index = s.indexOf( '\n' );
375
        final String message = s.substring( 0, index > 0 ? index : s.length() );
376
377
        getStatusBar().setText( message );
378
      }
379
    }
380
  }
381
382
  /**
383
   * Called when a file has been modified.
384
   *
385
   * @param file Path to the modified file.
386
   */
387
  private void update( final Path file ) {
388
    // Avoid throwing IllegalStateException by running from a non-JavaFX thread.
389
    Platform.runLater(
390
      () -> {
391
        // Brute-force XSLT file reload by re-instantiating all processors.
392
        resetProcessors();
393
        refreshSelectedTab( getActiveFileEditor() );
394
      }
395
    );
396
  }
397
398
  /**
399
   * After resetting the processors, they will refresh anew to be up-to-date
400
   * with the files (text and definition) currently loaded into the editor.
401
   */
402
  private void resetProcessors() {
403
    getProcessors().clear();
404
  }
405
406
  //---- File actions -------------------------------------------------------
407
  private void fileNew() {
408
    getFileEditorPane().newEditor();
409
  }
410
411
  private void fileOpen() {
412
    getFileEditorPane().openFileDialog();
413
  }
414
415
  private void fileClose() {
416
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
417
  }
418
419
  private void fileCloseAll() {
420
    getFileEditorPane().closeAllEditors();
421
  }
422
423
  private void fileSave() {
424
    getFileEditorPane().saveEditor( getActiveFileEditor() );
425
  }
426
427
  private void fileSaveAll() {
428
    getFileEditorPane().saveAllEditors();
429
  }
430
431
  private void fileExit() {
432
    final Window window = getWindow();
433
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
434
  }
435
436
  //---- Help actions -------------------------------------------------------
437
  private void helpAbout() {
438
    Alert alert = new Alert( AlertType.INFORMATION );
439
    alert.setTitle( get( "Dialog.about.title" ) );
440
    alert.setHeaderText( get( "Dialog.about.header" ) );
441
    alert.setContentText( get( "Dialog.about.content" ) );
442
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
443
    alert.initOwner( getWindow() );
444
445
    alert.showAndWait();
446
  }
447
448
  //---- Convenience accessors ----------------------------------------------
449
  private float getFloat( final String key, final float defaultValue ) {
450
    return getPreferences().getFloat( key, defaultValue );
451
  }
452
453
  private Preferences getPreferences() {
454
    return getOptions().getState();
455
  }
456
457
  protected Scene getScene() {
458
    if( this.scene == null ) {
459
      this.scene = createScene();
460
    }
461
462
    return this.scene;
463
  }
464
465
  public Window getWindow() {
466
    return getScene().getWindow();
467
  }
468
469
  private MarkdownEditorPane getActiveEditor() {
470
    final EditorPane pane = getActiveFileEditor().getEditorPane();
471
472
    return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
473
  }
474
475
  private FileEditorTab getActiveFileEditor() {
476
    return getFileEditorPane().getActiveFileEditor();
477
  }
478
479
  //---- Member accessors ---------------------------------------------------
480
  private void setScene( Scene scene ) {
481
    this.scene = scene;
482
  }
483
484
  private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) {
485
    this.processors = map;
486
  }
487
488
  private Map<FileEditorTab, Processor<String>> getProcessors() {
489
    if( this.processors == null ) {
490
      setProcessors( new HashMap<>() );
491
    }
492
493
    return this.processors;
494
  }
495
496
  private FileEditorTabPane getFileEditorPane() {
497
    if( this.fileEditorPane == null ) {
498
      this.fileEditorPane = createFileEditorPane();
499
    }
500
501
    return this.fileEditorPane;
502
  }
503
504
  private HTMLPreviewPane getPreviewPane() {
505
    if( this.previewPane == null ) {
506
      this.previewPane = createPreviewPane();
507
    }
508
509
    return this.previewPane;
510
  }
511
512
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
513
    this.definitionSource = definitionSource;
514
  }
515
516
  private DefinitionSource getDefinitionSource() {
517
    if( this.definitionSource == null ) {
518
      this.definitionSource = new EmptyDefinitionSource();
519
    }
520
521
    return this.definitionSource;
522
  }
523
524
  private DefinitionPane getDefinitionPane() {
525
    if( this.definitionPane == null ) {
526
      this.definitionPane = createDefinitionPane();
527
    }
528
529
    return this.definitionPane;
530
  }
531
532
  private Options getOptions() {
533
    return this.options;
534
  }
535
536
  private Snitch getSnitch() {
537
    return this.snitch;
538
  }
539
540
  private Notifier getNotifier() {
541
    return this.notifier;
542
  }
543
544
  public void setMenuBar( final MenuBar menuBar ) {
545
    this.menuBar = menuBar;
546
  }
547
548
  public MenuBar getMenuBar() {
549
    return this.menuBar;
550
  }
551
552
  private Text getLineNumberText() {
553
    if( this.lineNumberText == null ) {
554
      this.lineNumberText = createLineNumberText();
555
    }
556
557
    return this.lineNumberText;
558
  }
559
560
  private synchronized StatusBar getStatusBar() {
561
    if( this.statusBar == null ) {
562
      this.statusBar = createStatusBar();
563
    }
564
565
    return this.statusBar;
566
  }
567
568
  //---- Member creators ----------------------------------------------------
569
  /**
570
   * Factory to create processors that are suited to different file types.
571
   *
572
   * @param tab The tab that is subjected to processing.
573
   *
574
   * @return A processor suited to the file type specified by the tab's path.
575
   */
576
  private Processor<String> createProcessor( final FileEditorTab tab ) {
577
    return createProcessorFactory().createProcessor( tab );
578
  }
579
580
  private ProcessorFactory createProcessorFactory() {
581
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
582
  }
583
584
  private DefinitionSource createDefinitionSource( final String path ) {
585
    return createDefinitionFactory().createDefinitionSource( path );
586
  }
587
588
  /**
589
   * Create an editor pane to hold file editor tabs.
590
   *
591
   * @return A new instance, never null.
592
   */
593
  private FileEditorTabPane createFileEditorPane() {
594
    return new FileEditorTabPane();
595
  }
596
597
  private HTMLPreviewPane createPreviewPane() {
598
    return new HTMLPreviewPane();
599
  }
600
601
  private DefinitionPane createDefinitionPane() {
602
    return new DefinitionPane( getTreeView() );
603
  }
604
605
  private DefinitionFactory createDefinitionFactory() {
606
    return new DefinitionFactory();
607
  }
608
609
  private StatusBar createStatusBar() {
610
    return new StatusBar();
611
  }
612
613
  private Scene createScene() {
614
    final SplitPane splitPane = new SplitPane(
615
      getDefinitionPane().getNode(),
616
      getFileEditorPane().getNode(),
617
      getPreviewPane().getNode() );
618
619
    splitPane.setDividerPositions(
620
      getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
621
      getFloat( K_PANE_SPLIT_EDITOR, .45f ),
622
      getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
623
624
    // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
625
    final BorderPane borderPane = new BorderPane();
626
    borderPane.setPrefSize( 1024, 800 );
627
    borderPane.setTop( createMenuBar() );
628
    borderPane.setBottom( getStatusBar() );
629
    borderPane.setCenter( splitPane );
630
631
    final VBox box = new VBox();
632
    box.setAlignment( Pos.BASELINE_CENTER );
633
    box.getChildren().add( getLineNumberText() );
634
    getStatusBar().getRightItems().add( box );
635
636
    return new Scene( borderPane );
637
  }
638
639
  private Text createLineNumberText() {
640
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
634641
  }
635642
M src/main/java/com/scrivenvar/definition/AbstractDefinitionSource.java
2828
package com.scrivenvar.definition;
2929
30
import javafx.scene.control.TreeView;
31
3032
/**
3133
 * Implements common behaviour for definition sources.
32
 * 
34
 *
3335
 * @author White Magic Software, Ltd.
3436
 */
3537
public abstract class AbstractDefinitionSource implements DefinitionSource {
38
39
  private TreeView<String> treeView;
40
41
  /**
42
   * Returns this definition source as an editable graphical user interface
43
   * component.
44
   *
45
   * @return The TreeView for this definition source.
46
   */
47
  @Override
48
  public TreeView<String> asTreeView() {
49
50
    if( this.treeView == null ) {
51
      this.treeView = createTreeView();
52
      this.treeView.setEditable( true );
53
      this.treeView.setCellFactory(
54
        (TreeView<String> t) -> new TextFieldTreeCell()
55
      );
56
    }
57
58
    return this.treeView;
59
  }
60
61
  /**
62
   * Creates a newly instantiated tree view ready for adding to the definition
63
   * pane.
64
   *
65
   * @return A new tree view instance, never null.
66
   */
67
  protected abstract TreeView<String> createTreeView();
3668
}
3769
M src/main/java/com/scrivenvar/definition/DefinitionPane.java
7171
   * root node of the given
7272
   *
73
   * @param treeView The treeview containing a new root node; if the parameter
73
   * @param treeView The tree view containing a new root node; if the parameter
7474
   * is null, the tree is cleared.
7575
   */
7676
  public void setRoot( final TreeView<String> treeView ) {
7777
    getTreeView().setRoot( treeView == null ? null : treeView.getRoot() );
7878
  }
7979
8080
  /**
81
   * Clears the treeview by setting the root node to null.
81
   * Clears the tree view by setting the root node to null.
8282
   */
8383
  public void clear() {
M src/main/java/com/scrivenvar/definition/EmptyDefinitionSource.java
4242
  }
4343
44
  @Override
45
  public TreeView<String> asTreeView() {
46
    return new TreeView<>();
47
  }
4844
4945
  @Override
5046
  public Map<String, String> getResolvedMap() {
5147
    return new HashMap<>();
48
  }
49
50
  @Override
51
  protected TreeView<String> createTreeView() {
52
    return new TreeView<>();
5253
  }
5354
}
A src/main/java/com/scrivenvar/definition/TextFieldTreeCell.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import static com.scrivenvar.Messages.get;
31
import javafx.event.ActionEvent;
32
import javafx.scene.control.ContextMenu;
33
import javafx.scene.control.MenuItem;
34
import javafx.scene.control.TextField;
35
import javafx.scene.control.TreeCell;
36
import javafx.scene.control.TreeItem;
37
import static javafx.scene.input.KeyCode.ENTER;
38
import static javafx.scene.input.KeyCode.ESCAPE;
39
import javafx.scene.input.KeyEvent;
40
41
/**
42
 * Provides behaviour of adding, removing, and editing tree view items.
43
 *
44
 * @author White Magic Software, Ltd.
45
 */
46
public class TextFieldTreeCell extends TreeCell<String> {
47
48
  private TextField textField;
49
  private final ContextMenu editMenu = new ContextMenu();
50
51
  public TextFieldTreeCell() {
52
    initEditMenu();
53
  }
54
55
  private void initEditMenu() {
56
    final MenuItem addItem = createMenuItem( "Definition.menu.add" );
57
    final MenuItem removeItem = createMenuItem( "Definition.menu.remove" );
58
59
    addItem.setOnAction( (ActionEvent e) -> {
60
      final VariableTreeItem<String> treeItem = new VariableTreeItem<>( "Undefined" );
61
      getTreeItem().getChildren().add( treeItem );
62
    } );
63
64
    removeItem.setOnAction( (ActionEvent e) -> {
65
      final TreeItem c = getTreeItem();
66
      boolean remove = c.getParent().getChildren().remove( c );
67
      System.out.println( "Remove" );
68
    } );
69
70
    getEditMenu().getItems().add( addItem );
71
    getEditMenu().getItems().add( removeItem );
72
  }
73
74
  private ContextMenu getEditMenu() {
75
    return this.editMenu;
76
  }
77
78
  private MenuItem createMenuItem( String label ) {
79
    return new MenuItem( get( label ) );
80
  }
81
82
  @Override
83
  public void startEdit() {
84
    if( getTreeItem().isLeaf() ) {
85
      super.startEdit();
86
87
      final TextField inputField = getTextField();
88
89
      setText( null );
90
      setGraphic( inputField );
91
      inputField.selectAll();
92
      inputField.requestFocus();
93
    }
94
  }
95
96
  @Override
97
  public void cancelEdit() {
98
    super.cancelEdit();
99
100
    setText( (String)getItem() );
101
    setGraphic( getTreeItem().getGraphic() );
102
  }
103
104
  @Override
105
  public void updateItem( String item, boolean empty ) {
106
    super.updateItem( item, empty );
107
108
    if( empty ) {
109
      setText( null );
110
      setGraphic( null );
111
    }
112
    else if( isEditing() ) {
113
      TextField tf = getTextField();
114
      tf.setText( getItemValue() );
115
116
      setText( null );
117
      setGraphic( tf );
118
    }
119
    else {
120
      setText( getItemValue() );
121
      setGraphic( getTreeItem().getGraphic() );
122
123
      if( !getTreeItem().isLeaf() && getTreeItem().getParent() != null ) {
124
        setContextMenu( getEditMenu() );
125
      }
126
    }
127
  }
128
129
  private TextField createTextField() {
130
    final TextField tf = new TextField( getItemValue() );
131
132
    tf.setOnKeyReleased( (KeyEvent t) -> {
133
      switch( t.getCode() ) {
134
        case ENTER:
135
          commitEdit( tf.getText() );
136
          break;
137
        case ESCAPE:
138
          cancelEdit();
139
          break;
140
      }
141
    } );
142
143
    return tf;
144
  }
145
146
  /**
147
   * Returns the item's text value.
148
   *
149
   * @return A non-null String, possibly empty.
150
   */
151
  private String getItemValue() {
152
    return getItem() == null ? "" : getItem();
153
  }
154
155
  private TextField getTextField() {
156
    if( this.textField == null ) {
157
      this.textField = createTextField();
158
    }
159
160
    return this.textField;
161
  }
162
}
1163
M src/main/java/com/scrivenvar/definition/yaml/YamlFileDefinitionSource.java
4545
  private YamlTreeAdapter yamlTreeAdapter;
4646
  private YamlParser yamlParser;
47
  private TreeView<String> treeView;
4847
4948
  /**
...
5958
  private void init() {
6059
    setYamlParser( createYamlParser() );
61
  }
62
63
  /**
64
   * TODO: Associate variable file with path to current file.
65
   *
66
   * @return The TreeView for this definition source.
67
   */
68
  @Override
69
  public TreeView<String> asTreeView() {
70
    if( this.treeView == null ) {
71
      this.treeView = createTreeView();
72
    }
73
74
    return this.treeView;
7560
  }
7661
...
11297
  }
11398
114
  private TreeView<String> createTreeView() {
99
  @Override
100
  protected TreeView<String> createTreeView() {
115101
    return getYamlTreeAdapter().adapt(
116102
      get( "Pane.defintion.node.root.title" )
M src/main/java/com/scrivenvar/processors/CaretReplacementProcessor.java
5353
  @Override
5454
  public String processLink( final String t ) {
55
    return replace(t, CARET_POSITION_MD, CARET_POSITION_HTML );
55
    return replace( t, CARET_POSITION_MD, CARET_POSITION_HTML );
5656
  }
5757
5858
  /**
5959
   * Replaces the needle with thread in the given haystack. Based on Apache
60
   * Commons 3 StringUtils.replace method. Should be faster than
61
   * String.replace, which performs a little regex under the hood.
60
   * Commons 3 StringUtils.replace method. Should be faster than String.replace,
61
   * which performs a little regex under the hood.
6262
   *
6363
   * @param haystack Search this string for the needle, must not be null.
...
8080
    final int needleLength = needle.length();
8181
82
    int increase = thread.length() - needleLength;
83
    increase = (increase < 0 ? 0 : increase);
84
    final StringBuilder buffer = new StringBuilder( haystack.length() + increase );
82
    int len = thread.length() - needleLength;
83
    len = (len < 0 ? 0 : len);
84
    final StringBuilder buffer = new StringBuilder( haystack.length() + len );
8585
8686
    if( end != INDEX_NOT_FOUND ) {
M src/main/java/com/scrivenvar/processors/InlineRProcessor.java
2828
package com.scrivenvar.processors;
2929
30
import static com.scrivenvar.Constants.STATUS_PARSE_ERROR;
31
import static com.scrivenvar.Messages.get;
32
import com.scrivenvar.Services;
3033
import static com.scrivenvar.decorators.RVariableDecorator.PREFIX;
3134
import static com.scrivenvar.decorators.RVariableDecorator.SUFFIX;
3235
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
36
import com.scrivenvar.service.events.Notifier;
3337
import static java.lang.Math.min;
3438
import java.nio.file.Path;
...
4650
4751
  private ScriptEngine engine;
52
53
  private final Notifier notifier = Services.load( Notifier.class );
4854
4955
  /**
...
6571
  /**
6672
   * @see https://github.com/DaveJarvis/scrivenvar/issues/30
67
   * @param workingDirectory 
73
   * @param workingDirectory
6874
   */
6975
  private void init( final Path workingDirectory ) {
7076
    // In Windows, path characters must be changed from escape chars.
71
    eval( replace( ""
72
      + "assign( 'anchor', as.Date( '$date.anchor$', format='%Y-%m-%d' ), envir = .GlobalEnv );"
73
      + "setwd( '" + workingDirectory.toString().replace( '\\', '/' ) + "' );"
74
      + "source( '../bin/pluralize.R' );"
75
      + "source( '../bin/common.R' )", getDefinitions() ) );
77
    try {
78
      eval( replace( ""
79
        + "assign( 'anchor', as.Date( '$date.anchor$', format='%Y-%m-%d' ), envir = .GlobalEnv );"
80
        + "setwd( '" + workingDirectory.toString().replace( '\\', '/' ) + "' );"
81
        + "source( '../bin/pluralize.R' );"
82
        + "source( '../bin/common.R' )", getDefinitions() ) );
83
    } catch( final Exception e ) {
84
      throw new RuntimeException( e );
85
    }
7686
  }
7787
...
104114
105115
        // Pass the R statement into the R engine for evaluation.
106
        final Object result = eval( r );
116
        try {
117
          final Object result = eval( r );
107118
108
        // Append the string representation of the result into the text.
109
        sb.append( result );
119
          // Append the string representation of the result into the text.
120
          sb.append( result );
121
        } catch( final Exception e ) {
122
          // If the string couldn't be parsed using R, append the statement
123
          // that failed to parse, instead of its evaluated value.
124
          sb.append( PREFIX ).append( r ).append( SUFFIX );
125
126
          // Tell the user that there was a problem.
127
          getNotifier().notify( get( STATUS_PARSE_ERROR,
128
            e.getMessage(), currIndex )
129
          );
130
        }
110131
111132
        // Retain the R statement's ending position in the text.
112133
        prevIndex = currIndex + 1;
113
114134
      }
115135
      else {
...
135155
   * @return The object resulting from the evaluation.
136156
   */
137
  private Object eval( final String r ) {
138
    try {
139
      return getScriptEngine().eval( r );
140
    } catch( final ScriptException ex ) {
141
      throw new IllegalArgumentException( ex );
142
    }
157
  private Object eval( final String r ) throws ScriptException {
158
    return getScriptEngine().eval( r );
143159
  }
144160
145161
  private synchronized ScriptEngine getScriptEngine() {
146162
    if( this.engine == null ) {
147163
      this.engine = (new ScriptEngineManager()).getEngineByName( "Renjin" );
148164
    }
149165
150166
    return this.engine;
167
  }
168
169
  private Notifier getNotifier() {
170
    return this.notifier;
151171
  }
152172
}
M src/main/java/com/scrivenvar/service/events/Notifier.java
5454
   * Notifies the user about the exception.
5555
   *
56
   * @param exception The exception containing a message to show to the user.
56
   * @param ex The exception containing a message to show to the user.
5757
   */
58
  default public void notify( final Exception exception ) {
59
    notify( exception.getMessage() );
58
  default public void notify( final Exception ex ) {
59
    notify( ex.getMessage() );
6060
  }
6161
M src/main/java/com/scrivenvar/service/events/impl/DefaultNotifier.java
4343
 * @author White Magic Software, Ltd.
4444
 */
45
public final class DefaultNotifier extends Observable
46
  implements Notifier {
45
public final class DefaultNotifier extends Observable implements Notifier {
4746
4847
  public DefaultNotifier() {
...
5857
    setChanged();
5958
    notifyObservers( message );
59
  }
60
61
  @Override
62
  public void clear() {
63
    notify( STATUS_BAR_DEFAULT );
6064
  }
6165
...
7579
    final Object... args ) {
7680
    return new DefaultNotification( title, message, args );
77
  }
78
79
  @Override
80
  public void clear() {
81
    setChanged();
82
    notifyObservers( STATUS_BAR_DEFAULT );
8381
  }
8482
M src/main/java/com/scrivenvar/util/Action.java
2525
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2626
 */
27
2827
package com.scrivenvar.util;
2928
29
import de.jensd.fx.glyphs.GlyphIcons;
3030
import javafx.beans.value.ObservableBooleanValue;
3131
import javafx.event.ActionEvent;
3232
import javafx.event.EventHandler;
3333
import javafx.scene.input.KeyCombination;
34
import de.jensd.fx.glyphs.GlyphIcons;
3534
3635
/**
3736
 * Simple action class
3837
 *
3938
 * @author Karl Tauber
4039
 */
41
public class Action
42
{
43
	public final String text;
44
	public final KeyCombination accelerator;
45
	public final GlyphIcons icon;
46
	public final EventHandler<ActionEvent> action;
47
	public final ObservableBooleanValue disable;
40
public class Action {
4841
49
	public Action(String text, String accelerator, GlyphIcons icon,
50
		EventHandler<ActionEvent> action)
51
	{
52
		this(text, accelerator, icon, action, null);
53
	}
42
  public final String text;
43
  public final KeyCombination accelerator;
44
  public final GlyphIcons icon;
45
  public final EventHandler<ActionEvent> action;
46
  public final ObservableBooleanValue disable;
5447
55
	public Action(String text, String accelerator, GlyphIcons icon,
56
		EventHandler<ActionEvent> action, ObservableBooleanValue disable)
57
	{
58
		this.text = text;
59
		this.accelerator = (accelerator != null) ? KeyCombination.valueOf(accelerator) : null;
60
		this.icon = icon;
61
		this.action = action;
62
		this.disable = disable;
63
	}
48
  public Action(
49
    final String text,
50
    final String accelerator,
51
    final GlyphIcons icon,
52
    final EventHandler<ActionEvent> action ) {
53
    this( text, accelerator, icon, action, null );
54
  }
55
56
  public Action(
57
    final String text,
58
    final String accelerator,
59
    final GlyphIcons icon,
60
    final EventHandler<ActionEvent> action,
61
    final ObservableBooleanValue disable ) {
62
63
    this.text = text;
64
    this.accelerator = (accelerator != null)
65
      ? KeyCombination.valueOf( accelerator )
66
      : null;
67
    this.icon = icon;
68
    this.action = action;
69
    this.disable = disable;
70
  }
6471
}
6572
M src/main/resources/com/scrivenvar/messages.properties
9595
# ########################################################################
9696
97
Main.statusbar.line=Line {0} of {1}
97
Main.statusbar.text.offset=offset
98
Main.statusbar.line=Line {0} of {1}, ${Main.statusbar.text.offset} {2}
9899
Main.statusbar.state.default=OK
100
Main.statusbar.parse.error={0} (near ${Main.statusbar.text.offset} {1})
101
102
# ########################################################################
103
#
104
# Definition Pane and its Tree View
105
#
106
# ########################################################################
107
108
Definition.menu.add=Add
109
Definition.menu.remove=Delete
99110
100111
# ########################################################################