Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M build.gradle
6969
def launcherClassName = "com.${applicationName}.Launcher"
7070
71
def propertiesFile = new File("src/main/resources/com/$applicationName/app.properties")
71
def propertiesFile = new File("src/main/resources/com/${applicationName}/app.properties")
7272
propertiesFile.write("application.version=${version}")
7373
M src/main/java/com/scrivenvar/Launcher.java
5555
5656
  private static void showAppInfo() throws IOException {
57
    out( format( "%s version %s%n", getTitle(), getVersion() ) );
58
    out( format( "Copyright %s by White Magic Software, Ltd.%n", getYear() ) );
59
    out( "Portions copyright 2020 Karl Tauber.\n" );
57
    out( format( "%s version %s", getTitle(), getVersion() ) );
58
    out( format( "Copyright %s by White Magic Software, Ltd.", getYear() ) );
59
    out( format( "Portions copyright 2020 Karl Tauber." ) );
6060
  }
6161
6262
  private static void out( final String s ) {
63
    System.out.print( s );
63
    System.out.println( s );
6464
  }
6565
M src/main/java/com/scrivenvar/MainWindow.java
7070
import javafx.stage.Window;
7171
import javafx.stage.WindowEvent;
72
import org.controlsfx.control.StatusBar;
73
import org.fxmisc.richtext.model.TwoDimensional.Position;
74
75
import java.io.File;
76
import java.nio.file.Path;
77
import java.util.*;
78
import java.util.function.Function;
79
import java.util.prefs.Preferences;
80
81
import static com.scrivenvar.Constants.*;
82
import static com.scrivenvar.Messages.get;
83
import static com.scrivenvar.Messages.getLiteral;
84
import static com.scrivenvar.util.StageState.*;
85
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
86
import static javafx.event.Event.fireEvent;
87
import static javafx.scene.input.KeyCode.ENTER;
88
import static javafx.scene.input.KeyCode.TAB;
89
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
90
91
/**
92
 * Main window containing a tab pane in the center for file editors.
93
 *
94
 * @author Karl Tauber and White Magic Software, Ltd.
95
 */
96
public class MainWindow implements Observer {
97
98
  private final Options mOptions = Services.load( Options.class );
99
  private final Snitch mSnitch = Services.load( Snitch.class );
100
  private final Settings mSettings = Services.load( Settings.class );
101
  private final Notifier mNotifier = Services.load( Notifier.class );
102
103
  private final Scene mScene;
104
  private final StatusBar mStatusBar;
105
  private final Text mLineNumberText;
106
  private final TextField mFindTextField;
107
108
  private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
109
  private final DefinitionPane mDefinitionPane = new DefinitionPane();
110
  private final HTMLPreviewPane mPreviewPane = new HTMLPreviewPane();
111
  private FileEditorTabPane fileEditorPane;
112
113
  /**
114
   * Prevents re-instantiation of processing classes.
115
   */
116
  private final Map<FileEditorTab, Processor<String>> mProcessors =
117
      new HashMap<>();
118
119
  private final Map<String, String> mResolvedMap =
120
      new HashMap<>( DEFAULT_MAP_SIZE );
121
122
  /**
123
   * Listens on the definition pane for double-click events.
124
   */
125
  private VariableNameInjector variableNameInjector;
126
127
  /**
128
   * Called when the definition data is changed.
129
   */
130
  final EventHandler<TreeItem.TreeModificationEvent<Event>> mTreeHandler =
131
      event -> {
132
        exportDefinitions( getDefinitionPath() );
133
        interpolateResolvedMap();
134
        refreshActiveTab();
135
      };
136
137
  final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
138
      event -> {
139
        if( event.getCode() == ENTER ) {
140
          getVariableNameInjector().injectSelectedItem();
141
        }
142
      };
143
144
  final EventHandler<? super KeyEvent> mEditorKeyHandler =
145
      (EventHandler<KeyEvent>) event -> {
146
        if( event.getCode() == TAB ) {
147
          getDefinitionPane().requestFocus();
148
          event.consume();
149
        }
150
      };
151
152
  public MainWindow() {
153
    mStatusBar = createStatusBar();
154
    mLineNumberText = createLineNumberText();
155
    mFindTextField = createFindTextField();
156
    mScene = createScene();
157
158
    initLayout();
159
    initFindInput();
160
    initSnitch();
161
    initDefinitionListener();
162
    initTabAddedListener();
163
    initTabChangedListener();
164
    initPreferences();
165
  }
166
167
  /**
168
   * Watch for changes to external files. In particular, this awaits
169
   * modifications to any XSL files associated with XML files being edited. When
170
   * an XSL file is modified (external to the application), the snitch's ears
171
   * perk up and the file is reloaded. This keeps the XSL transformation up to
172
   * date with what's on the file system.
173
   */
174
  private void initSnitch() {
175
    getSnitch().addObserver( this );
176
  }
177
178
  /**
179
   * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
180
   * presses.
181
   */
182
  private void initFindInput() {
183
    final TextField input = getFindTextField();
184
185
    input.setOnKeyPressed( ( KeyEvent event ) -> {
186
      switch( event.getCode() ) {
187
        case F3:
188
        case ENTER:
189
          findNext();
190
          break;
191
        case F:
192
          if( !event.isControlDown() ) {
193
            break;
194
          }
195
        case ESCAPE:
196
          getStatusBar().setGraphic( null );
197
          getActiveFileEditor().getEditorPane().requestFocus();
198
          break;
199
      }
200
    } );
201
202
    // Remove when the input field loses focus.
203
    input.focusedProperty().addListener(
204
        (
205
            final ObservableValue<? extends Boolean> focused,
206
            final Boolean oFocus,
207
            final Boolean nFocus ) -> {
208
          if( !nFocus ) {
209
            getStatusBar().setGraphic( null );
210
          }
211
        }
212
    );
213
  }
214
215
  /**
216
   * Listen for {@link FileEditorTabPane} to receive open definition file event.
217
   */
218
  private void initDefinitionListener() {
219
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
220
        ( final ObservableValue<? extends Path> file,
221
          final Path oldPath, final Path newPath ) -> {
222
          // Indirectly refresh the resolved map.
223
          resetProcessors();
224
225
          openDefinitions( newPath );
226
227
          // Will create new processors and therefore a new resolved map.
228
          refreshActiveTab();
229
        }
230
    );
231
  }
232
233
  /**
234
   * When tabs are added, hook the various change listeners onto the new tab so
235
   * that the preview pane refreshes as necessary.
236
   */
237
  private void initTabAddedListener() {
238
    final FileEditorTabPane editorPane = getFileEditorPane();
239
240
    // Make sure the text processor kicks off when new files are opened.
241
    final ObservableList<Tab> tabs = editorPane.getTabs();
242
243
    // Update the preview pane on tab changes.
244
    tabs.addListener(
245
        ( final Change<? extends Tab> change ) -> {
246
          while( change.next() ) {
247
            if( change.wasAdded() ) {
248
              // Multiple tabs can be added simultaneously.
249
              for( final Tab newTab : change.getAddedSubList() ) {
250
                final FileEditorTab tab = (FileEditorTab) newTab;
251
252
                initTextChangeListener( tab );
253
                initCaretParagraphListener( tab );
254
                initKeyboardEventListeners( tab );
255
//              initSyntaxListener( tab );
256
              }
257
            }
258
          }
259
        }
260
    );
261
  }
262
263
  /**
264
   * Reloads the preferences from the previous session.
265
   */
266
  private void initPreferences() {
267
    restoreDefinitionPane();
268
    getFileEditorPane().restorePreferences();
269
  }
270
271
  /**
272
   * Listen for new tab selection events.
273
   */
274
  private void initTabChangedListener() {
275
    final FileEditorTabPane editorPane = getFileEditorPane();
276
277
    // Update the preview pane changing tabs.
278
    editorPane.addTabSelectionListener(
279
        ( ObservableValue<? extends Tab> tabPane,
280
          final Tab oldTab, final Tab newTab ) -> {
281
          updateVariableNameInjector();
282
283
          // If there was no old tab, then this is a first time load, which
284
          // can be ignored.
285
          if( oldTab != null ) {
286
            if( newTab == null ) {
287
              closeRemainingTab();
288
            }
289
            else {
290
              // Update the preview with the edited text.
291
              refreshSelectedTab( (FileEditorTab) newTab );
292
            }
293
          }
294
        }
295
    );
296
  }
297
298
  /**
299
   * Ensure that the keyboard events are received when a new tab is added
300
   * to the user interface.
301
   *
302
   * @param tab The tab that can trigger keyboard events, such as control+space.
303
   */
304
  private void initKeyboardEventListeners( final FileEditorTab tab ) {
305
    final VariableNameInjector vin = getVariableNameInjector();
306
    vin.initKeyboardEventListeners( tab );
307
308
    tab.addEventFilter( KeyEvent.KEY_PRESSED, mEditorKeyHandler );
309
  }
310
311
  private void initTextChangeListener( final FileEditorTab tab ) {
312
    tab.addTextChangeListener(
313
        ( ObservableValue<? extends String> editor,
314
          final String oldValue, final String newValue ) ->
315
            refreshSelectedTab( tab )
316
    );
317
  }
318
319
  private void initCaretParagraphListener( final FileEditorTab tab ) {
320
    tab.addCaretParagraphListener(
321
        ( ObservableValue<? extends Integer> editor,
322
          final Integer oldValue, final Integer newValue ) ->
323
            refreshSelectedTab( tab )
324
    );
325
  }
326
327
  private void updateVariableNameInjector() {
328
    getVariableNameInjector().setFileEditorTab( getActiveFileEditor() );
329
  }
330
331
  private void setVariableNameInjector( final VariableNameInjector injector ) {
332
    this.variableNameInjector = injector;
333
  }
334
335
  private synchronized VariableNameInjector getVariableNameInjector() {
336
    if( this.variableNameInjector == null ) {
337
      final VariableNameInjector vin = createVariableNameInjector();
338
      setVariableNameInjector( vin );
339
    }
340
341
    return this.variableNameInjector;
342
  }
343
344
  private VariableNameInjector createVariableNameInjector() {
345
    final FileEditorTab tab = getActiveFileEditor();
346
    final DefinitionPane pane = getDefinitionPane();
347
348
    return new VariableNameInjector( tab, pane );
349
  }
350
351
  /**
352
   * Called whenever the preview pane becomes out of sync with the file editor
353
   * tab. This can be called when the text changes, the caret paragraph changes,
354
   * or the file tab changes.
355
   *
356
   * @param tab The file editor tab that has been changed in some fashion.
357
   */
358
  private void refreshSelectedTab( final FileEditorTab tab ) {
359
    if( tab == null ) {
360
      return;
361
    }
362
363
    getPreviewPane().setPath( tab.getPath() );
364
365
    // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
366
    final Position p = tab.getCaretOffset();
367
    getLineNumberText().setText(
368
        get( STATUS_BAR_LINE,
369
             p.getMajor() + 1,
370
             p.getMinor() + 1,
371
             tab.getCaretPosition() + 1
372
        )
373
    );
374
375
    Processor<String> processor = getProcessors().get( tab );
376
377
    if( processor == null ) {
378
      processor = createProcessor( tab );
379
      getProcessors().put( tab, processor );
380
    }
381
382
    try {
383
      processor.processChain( tab.getEditorText() );
384
    } catch( final Exception ex ) {
385
      error( ex );
386
    }
387
  }
388
389
  private void refreshActiveTab() {
390
    refreshSelectedTab( getActiveFileEditor() );
391
  }
392
393
  /**
394
   * Used to find text in the active file editor window.
395
   */
396
  private void find() {
397
    final TextField input = getFindTextField();
398
    getStatusBar().setGraphic( input );
399
    input.requestFocus();
400
  }
401
402
  public void findNext() {
403
    getActiveFileEditor().searchNext( getFindTextField().getText() );
404
  }
405
406
  /**
407
   * Returns the variable map of interpolated definitions.
408
   *
409
   * @return A map to help dereference variables.
410
   */
411
  private Map<String, String> getResolvedMap() {
412
    return mResolvedMap;
413
  }
414
415
  private void interpolateResolvedMap() {
416
    final Map<String, String> treeMap = getDefinitionPane().toMap();
417
    final Map<String, String> map = new HashMap<>( treeMap );
418
    MapInterpolator.interpolate( map );
419
420
    getResolvedMap().clear();
421
    getResolvedMap().putAll( map );
422
  }
423
424
  /**
425
   * Called when a definition source is opened.
426
   *
427
   * @param path Path to the definition source that was opened.
428
   */
429
  private void openDefinitions( final Path path ) {
430
    try {
431
      final DefinitionSource ds = createDefinitionSource( path );
432
      setDefinitionSource( ds );
433
      storeDefinitionSourceFilename( path );
434
435
      final DefinitionPane pane = getDefinitionPane();
436
      pane.update( ds );
437
      pane.addTreeChangeHandler( mTreeHandler );
438
      pane.addKeyEventHandler( mDefinitionKeyHandler );
439
440
      interpolateResolvedMap();
441
    } catch( final Exception e ) {
442
      error( e );
443
    }
444
  }
445
446
  private void exportDefinitions( final Path path ) {
447
    try {
448
      final DefinitionPane pane = getDefinitionPane();
449
      final TreeItem<String> root = pane.getTreeView().getRoot();
450
      final TreeItem<String> problemChild = pane.isTreeWellFormed();
451
452
      if( problemChild == null ) {
453
        getDefinitionSource().getTreeAdapter().export( root, path );
454
        getNotifier().clear();
455
      }
456
      else {
457
        final String msg = get( "yaml.error.tree.form",
458
                                problemChild.getValue() );
459
        getNotifier().notify( msg );
460
      }
461
    } catch( final Exception e ) {
462
      error( e );
463
    }
464
  }
465
466
  private Path getDefinitionPath() {
467
    final String source = getPreferences().get(
468
        PERSIST_DEFINITION_SOURCE, "" );
469
470
    return new File(
471
        source.isBlank()
472
            ? getSetting( "file.definition.default", "variables.yaml" )
473
            : source
474
    ).toPath();
475
  }
476
477
  private void restoreDefinitionPane() {
478
    openDefinitions( getDefinitionPath() );
479
  }
480
481
  private void storeDefinitionSourceFilename( final Path path ) {
482
    getPreferences().put( PERSIST_DEFINITION_SOURCE, path.toString() );
483
  }
484
485
  /**
486
   * Called when the last open tab is closed to clear the preview pane.
487
   */
488
  private void closeRemainingTab() {
489
    getPreviewPane().clear();
490
  }
491
492
  /**
493
   * Called when an exception occurs that warrants the user's attention.
494
   *
495
   * @param e The exception with a message that the user should know about.
496
   */
497
  private void error( final Exception e ) {
498
    getNotifier().notify( e );
499
  }
500
501
  //---- File actions -------------------------------------------------------
502
503
  /**
504
   * Called when an observable instance has changed. This is called by both the
505
   * snitch service and the notify service. The snitch service can be called for
506
   * different file types, including definition sources.
507
   *
508
   * @param observable The observed instance.
509
   * @param value      The noteworthy item.
510
   */
511
  @Override
512
  public void update( final Observable observable, final Object value ) {
513
    if( value != null ) {
514
      if( observable instanceof Snitch && value instanceof Path ) {
515
        updateSelectedTab();
516
      }
517
      else if( observable instanceof Notifier && value instanceof String ) {
518
        updateStatusBar( (String) value );
519
      }
520
    }
521
  }
522
523
  /**
524
   * Updates the status bar to show the given message.
525
   *
526
   * @param s The message to show in the status bar.
527
   */
528
  private void updateStatusBar( final String s ) {
529
    Platform.runLater(
530
        () -> {
531
          final int index = s.indexOf( '\n' );
532
          final String message = s.substring(
533
              0, index > 0 ? index : s.length() );
534
535
          getStatusBar().setText( message );
536
        }
537
    );
538
  }
539
540
  /**
541
   * Called when a file has been modified.
542
   */
543
  private void updateSelectedTab() {
544
    Platform.runLater(
545
        () -> {
546
          // Brute-force XSLT file reload by re-instantiating all processors.
547
          resetProcessors();
548
          refreshActiveTab();
549
        }
550
    );
551
  }
552
553
  /**
554
   * After resetting the processors, they will refresh anew to be up-to-date
555
   * with the files (text and definition) currently loaded into the editor.
556
   */
557
  private void resetProcessors() {
558
    getProcessors().clear();
559
  }
560
561
  //---- File actions -------------------------------------------------------
562
  private void fileNew() {
563
    getFileEditorPane().newEditor();
564
  }
565
566
  private void fileOpen() {
567
    getFileEditorPane().openFileDialog();
568
  }
569
570
  private void fileClose() {
571
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
572
  }
573
574
  private void fileCloseAll() {
575
    getFileEditorPane().closeAllEditors();
576
  }
577
578
  private void fileSave() {
579
    getFileEditorPane().saveEditor( getActiveFileEditor() );
580
  }
581
582
  private void fileSaveAs() {
583
    final FileEditorTab editor = getActiveFileEditor();
584
    getFileEditorPane().saveEditorAs( editor );
585
    getProcessors().remove( editor );
586
587
    try {
588
      refreshSelectedTab( editor );
589
    } catch( final Exception ex ) {
590
      getNotifier().notify( ex );
591
    }
592
  }
593
594
  private void fileSaveAll() {
595
    getFileEditorPane().saveAllEditors();
596
  }
597
598
  private void fileExit() {
599
    final Window window = getWindow();
600
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
601
  }
602
603
  //---- R menu actions
604
  private void rScript() {
605
    final String script = getPreferences().get( PERSIST_R_STARTUP, "" );
606
    final RScriptDialog dialog = new RScriptDialog(
607
        getWindow(), "Dialog.r.script.title", script );
608
    final Optional<String> result = dialog.showAndWait();
609
610
    result.ifPresent( this::putStartupScript );
611
  }
612
613
  private void rDirectory() {
614
    final TextInputDialog dialog = new TextInputDialog(
615
        getPreferences().get( PERSIST_R_DIRECTORY, USER_DIRECTORY )
616
    );
617
618
    dialog.setTitle( get( "Dialog.r.directory.title" ) );
619
    dialog.setHeaderText( getLiteral( "Dialog.r.directory.header" ) );
620
    dialog.setContentText( "Directory" );
621
622
    final Optional<String> result = dialog.showAndWait();
623
624
    result.ifPresent( this::putStartupDirectory );
625
  }
626
627
  /**
628
   * Stores the R startup script into the user preferences.
629
   */
630
  private void putStartupScript( final String script ) {
631
    putPreference( PERSIST_R_STARTUP, script );
632
  }
633
634
  /**
635
   * Stores the R bootstrap script directory into the user preferences.
636
   */
637
  private void putStartupDirectory( final String directory ) {
638
    putPreference( PERSIST_R_DIRECTORY, directory );
639
  }
640
641
  //---- Help actions -------------------------------------------------------
642
  private void helpAbout() {
643
    final Alert alert = new Alert( AlertType.INFORMATION );
644
    alert.setTitle( get( "Dialog.about.title" ) );
645
    alert.setHeaderText( get( "Dialog.about.header" ) );
646
    alert.setContentText( get( "Dialog.about.content" ) );
647
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
648
    alert.initOwner( getWindow() );
649
650
    alert.showAndWait();
651
  }
652
653
  //---- Convenience accessors ----------------------------------------------
654
  private float getFloat( final String key, final float defaultValue ) {
655
    return getPreferences().getFloat( key, defaultValue );
656
  }
657
658
  private Preferences getPreferences() {
659
    return getOptions().getState();
660
  }
661
662
  protected Scene getScene() {
663
    return mScene;
664
  }
665
666
  public Window getWindow() {
667
    return getScene().getWindow();
668
  }
669
670
  private MarkdownEditorPane getActiveEditor() {
671
    final EditorPane pane = getActiveFileEditor().getEditorPane();
672
673
    return pane instanceof MarkdownEditorPane
674
        ? (MarkdownEditorPane) pane
675
        : null;
676
  }
677
678
  private FileEditorTab getActiveFileEditor() {
679
    return getFileEditorPane().getActiveFileEditor();
680
  }
681
682
  //---- Member accessors ---------------------------------------------------
683
684
  private Map<FileEditorTab, Processor<String>> getProcessors() {
685
    return mProcessors;
686
  }
687
688
  private FileEditorTabPane getFileEditorPane() {
689
    if( this.fileEditorPane == null ) {
690
      this.fileEditorPane = createFileEditorPane();
691
    }
692
693
    return this.fileEditorPane;
694
  }
695
696
  private HTMLPreviewPane getPreviewPane() {
697
    return mPreviewPane;
698
  }
699
700
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
701
    assert definitionSource != null;
702
    mDefinitionSource = definitionSource;
703
  }
704
705
  private DefinitionSource getDefinitionSource() {
706
    return mDefinitionSource;
707
  }
708
709
  private DefinitionPane getDefinitionPane() {
710
    return mDefinitionPane;
711
  }
712
713
  private Options getOptions() {
714
    return mOptions;
715
  }
716
717
  private Snitch getSnitch() {
718
    return mSnitch;
719
  }
720
721
  private Notifier getNotifier() {
722
    return mNotifier;
723
  }
724
725
  private Text getLineNumberText() {
726
    return mLineNumberText;
727
  }
728
729
  private StatusBar getStatusBar() {
730
    return mStatusBar;
731
  }
732
733
  private TextField getFindTextField() {
734
    return mFindTextField;
735
  }
736
737
  //---- Member creators ----------------------------------------------------
738
739
  /**
740
   * Factory to create processors that are suited to different file types.
741
   *
742
   * @param tab The tab that is subjected to processing.
743
   * @return A processor suited to the file type specified by the tab's path.
744
   */
745
  private Processor<String> createProcessor( final FileEditorTab tab ) {
746
    return createProcessorFactory().createProcessor( tab );
747
  }
748
749
  private ProcessorFactory createProcessorFactory() {
750
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
751
  }
752
753
  private DefinitionSource createDefaultDefinitionSource() {
754
    return new YamlDefinitionSource( getDefinitionPath() );
755
  }
756
757
  private DefinitionSource createDefinitionSource( final Path path ) {
758
    try {
759
      return createDefinitionFactory().createDefinitionSource( path );
760
    } catch( final Exception ex ) {
761
      error( ex );
762
      return createDefaultDefinitionSource();
763
    }
764
  }
765
766
  private TextField createFindTextField() {
767
    return new TextField();
768
  }
769
770
  /**
771
   * Create an editor pane to hold file editor tabs.
772
   *
773
   * @return A new instance, never null.
774
   */
775
  private FileEditorTabPane createFileEditorPane() {
776
    return new FileEditorTabPane();
777
  }
778
779
  private DefinitionFactory createDefinitionFactory() {
780
    return new DefinitionFactory();
781
  }
782
783
  private StatusBar createStatusBar() {
784
    return new StatusBar();
785
  }
786
787
  private Scene createScene() {
788
    final SplitPane splitPane = new SplitPane(
789
        getDefinitionPane().getNode(),
790
        getFileEditorPane().getNode(),
791
        getPreviewPane().getNode() );
792
793
    splitPane.setDividerPositions(
794
        getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
795
        getFloat( K_PANE_SPLIT_EDITOR, .45f ),
796
        getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
797
798
    // See: http://broadlyapplicable.blogspot
799
    // .ca/2015/03/javafx-capture-restorePreferences-splitpane.html
800
    final BorderPane borderPane = new BorderPane();
801
    borderPane.setPrefSize( 1024, 800 );
802
    borderPane.setTop( createMenuBar() );
803
    borderPane.setBottom( getStatusBar() );
804
    borderPane.setCenter( splitPane );
805
806
    final VBox box = new VBox();
807
    box.setAlignment( Pos.BASELINE_CENTER );
808
    box.getChildren().add( getLineNumberText() );
809
    getStatusBar().getRightItems().add( box );
72
import javafx.util.Duration;
73
import org.controlsfx.control.StatusBar;
74
import org.fxmisc.richtext.model.TwoDimensional.Position;
75
76
import java.io.File;
77
import java.nio.file.Path;
78
import java.util.*;
79
import java.util.function.Function;
80
import java.util.prefs.Preferences;
81
82
import static com.scrivenvar.Constants.*;
83
import static com.scrivenvar.Messages.get;
84
import static com.scrivenvar.Messages.getLiteral;
85
import static com.scrivenvar.util.StageState.*;
86
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
87
import static javafx.event.Event.fireEvent;
88
import static javafx.scene.input.KeyCode.ENTER;
89
import static javafx.scene.input.KeyCode.TAB;
90
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
91
92
/**
93
 * Main window containing a tab pane in the center for file editors.
94
 *
95
 * @author Karl Tauber and White Magic Software, Ltd.
96
 */
97
public class MainWindow implements Observer {
98
99
  private final Options mOptions = Services.load( Options.class );
100
  private final Snitch mSnitch = Services.load( Snitch.class );
101
  private final Settings mSettings = Services.load( Settings.class );
102
  private final Notifier mNotifier = Services.load( Notifier.class );
103
104
  private final Scene mScene;
105
  private final StatusBar mStatusBar;
106
  private final Text mLineNumberText;
107
  private final TextField mFindTextField;
108
109
  private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
110
  private final DefinitionPane mDefinitionPane = new DefinitionPane();
111
  private final HTMLPreviewPane mPreviewPane = new HTMLPreviewPane();
112
  private FileEditorTabPane fileEditorPane;
113
114
  /**
115
   * Prevents re-instantiation of processing classes.
116
   */
117
  private final Map<FileEditorTab, Processor<String>> mProcessors =
118
      new HashMap<>();
119
120
  private final Map<String, String> mResolvedMap =
121
      new HashMap<>( DEFAULT_MAP_SIZE );
122
123
  /**
124
   * Listens on the definition pane for double-click events.
125
   */
126
  private VariableNameInjector variableNameInjector;
127
128
  /**
129
   * Called when the definition data is changed.
130
   */
131
  final EventHandler<TreeItem.TreeModificationEvent<Event>> mTreeHandler =
132
      event -> {
133
        exportDefinitions( getDefinitionPath() );
134
        interpolateResolvedMap();
135
        refreshActiveTab();
136
      };
137
138
  final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
139
      event -> {
140
        if( event.getCode() == ENTER ) {
141
          getVariableNameInjector().injectSelectedItem();
142
        }
143
      };
144
145
  final EventHandler<? super KeyEvent> mEditorKeyHandler =
146
      (EventHandler<KeyEvent>) event -> {
147
        if( event.getCode() == TAB ) {
148
          getDefinitionPane().requestFocus();
149
          event.consume();
150
        }
151
      };
152
153
  public MainWindow() {
154
    mStatusBar = createStatusBar();
155
    mLineNumberText = createLineNumberText();
156
    mFindTextField = createFindTextField();
157
    mScene = createScene();
158
159
    initLayout();
160
    initFindInput();
161
    initSnitch();
162
    initDefinitionListener();
163
    initTabAddedListener();
164
    initTabChangedListener();
165
    initPreferences();
166
  }
167
168
  /**
169
   * Watch for changes to external files. In particular, this awaits
170
   * modifications to any XSL files associated with XML files being edited. When
171
   * an XSL file is modified (external to the application), the snitch's ears
172
   * perk up and the file is reloaded. This keeps the XSL transformation up to
173
   * date with what's on the file system.
174
   */
175
  private void initSnitch() {
176
    getSnitch().addObserver( this );
177
  }
178
179
  /**
180
   * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
181
   * presses.
182
   */
183
  private void initFindInput() {
184
    final TextField input = getFindTextField();
185
186
    input.setOnKeyPressed( ( KeyEvent event ) -> {
187
      switch( event.getCode() ) {
188
        case F3:
189
        case ENTER:
190
          findNext();
191
          break;
192
        case F:
193
          if( !event.isControlDown() ) {
194
            break;
195
          }
196
        case ESCAPE:
197
          getStatusBar().setGraphic( null );
198
          getActiveFileEditor().getEditorPane().requestFocus();
199
          break;
200
      }
201
    } );
202
203
    // Remove when the input field loses focus.
204
    input.focusedProperty().addListener(
205
        (
206
            final ObservableValue<? extends Boolean> focused,
207
            final Boolean oFocus,
208
            final Boolean nFocus ) -> {
209
          if( !nFocus ) {
210
            getStatusBar().setGraphic( null );
211
          }
212
        }
213
    );
214
  }
215
216
  /**
217
   * Listen for {@link FileEditorTabPane} to receive open definition file event.
218
   */
219
  private void initDefinitionListener() {
220
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
221
        ( final ObservableValue<? extends Path> file,
222
          final Path oldPath, final Path newPath ) -> {
223
          // Indirectly refresh the resolved map.
224
          resetProcessors();
225
226
          openDefinitions( newPath );
227
228
          // Will create new processors and therefore a new resolved map.
229
          refreshActiveTab();
230
        }
231
    );
232
  }
233
234
  /**
235
   * When tabs are added, hook the various change listeners onto the new tab so
236
   * that the preview pane refreshes as necessary.
237
   */
238
  private void initTabAddedListener() {
239
    final FileEditorTabPane editorPane = getFileEditorPane();
240
241
    // Make sure the text processor kicks off when new files are opened.
242
    final ObservableList<Tab> tabs = editorPane.getTabs();
243
244
    // Update the preview pane on tab changes.
245
    tabs.addListener(
246
        ( final Change<? extends Tab> change ) -> {
247
          while( change.next() ) {
248
            if( change.wasAdded() ) {
249
              // Multiple tabs can be added simultaneously.
250
              for( final Tab newTab : change.getAddedSubList() ) {
251
                final FileEditorTab tab = (FileEditorTab) newTab;
252
253
                initTextChangeListener( tab );
254
                initCaretParagraphListener( tab );
255
                initKeyboardEventListeners( tab );
256
//              initSyntaxListener( tab );
257
              }
258
            }
259
          }
260
        }
261
    );
262
  }
263
264
  /**
265
   * Reloads the preferences from the previous session.
266
   */
267
  private void initPreferences() {
268
    restoreDefinitionPane();
269
    getFileEditorPane().restorePreferences();
270
  }
271
272
  /**
273
   * Listen for new tab selection events.
274
   */
275
  private void initTabChangedListener() {
276
    final FileEditorTabPane editorPane = getFileEditorPane();
277
278
    // Update the preview pane changing tabs.
279
    editorPane.addTabSelectionListener(
280
        ( ObservableValue<? extends Tab> tabPane,
281
          final Tab oldTab, final Tab newTab ) -> {
282
          updateVariableNameInjector();
283
284
          // If there was no old tab, then this is a first time load, which
285
          // can be ignored.
286
          if( oldTab != null ) {
287
            if( newTab == null ) {
288
              closeRemainingTab();
289
            }
290
            else {
291
              // Update the preview with the edited text.
292
              refreshSelectedTab( (FileEditorTab) newTab );
293
            }
294
          }
295
        }
296
    );
297
  }
298
299
  /**
300
   * Ensure that the keyboard events are received when a new tab is added
301
   * to the user interface.
302
   *
303
   * @param tab The tab that can trigger keyboard events, such as control+space.
304
   */
305
  private void initKeyboardEventListeners( final FileEditorTab tab ) {
306
    final VariableNameInjector vin = getVariableNameInjector();
307
    vin.initKeyboardEventListeners( tab );
308
309
    tab.addEventFilter( KeyEvent.KEY_PRESSED, mEditorKeyHandler );
310
  }
311
312
  private void initTextChangeListener( final FileEditorTab tab ) {
313
    tab.addTextChangeListener(
314
        ( ObservableValue<? extends String> editor,
315
          final String oldValue, final String newValue ) ->
316
            refreshSelectedTab( tab )
317
    );
318
  }
319
320
  private void initCaretParagraphListener( final FileEditorTab tab ) {
321
    tab.addCaretParagraphListener(
322
        ( ObservableValue<? extends Integer> editor,
323
          final Integer oldValue, final Integer newValue ) ->
324
            refreshSelectedTab( tab )
325
    );
326
  }
327
328
  private void updateVariableNameInjector() {
329
    getVariableNameInjector().setFileEditorTab( getActiveFileEditor() );
330
  }
331
332
  private void setVariableNameInjector( final VariableNameInjector injector ) {
333
    this.variableNameInjector = injector;
334
  }
335
336
  private synchronized VariableNameInjector getVariableNameInjector() {
337
    if( this.variableNameInjector == null ) {
338
      final VariableNameInjector vin = createVariableNameInjector();
339
      setVariableNameInjector( vin );
340
    }
341
342
    return this.variableNameInjector;
343
  }
344
345
  private VariableNameInjector createVariableNameInjector() {
346
    final FileEditorTab tab = getActiveFileEditor();
347
    final DefinitionPane pane = getDefinitionPane();
348
349
    return new VariableNameInjector( tab, pane );
350
  }
351
352
  /**
353
   * Called whenever the preview pane becomes out of sync with the file editor
354
   * tab. This can be called when the text changes, the caret paragraph changes,
355
   * or the file tab changes.
356
   *
357
   * @param tab The file editor tab that has been changed in some fashion.
358
   */
359
  private void refreshSelectedTab( final FileEditorTab tab ) {
360
    if( tab == null ) {
361
      return;
362
    }
363
364
    getPreviewPane().setPath( tab.getPath() );
365
366
    // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
367
    final Position p = tab.getCaretOffset();
368
    getLineNumberText().setText(
369
        get( STATUS_BAR_LINE,
370
             p.getMajor() + 1,
371
             p.getMinor() + 1,
372
             tab.getCaretPosition() + 1
373
        )
374
    );
375
376
    Processor<String> processor = getProcessors().get( tab );
377
378
    if( processor == null ) {
379
      processor = createProcessor( tab );
380
      getProcessors().put( tab, processor );
381
    }
382
383
    try {
384
      processor.processChain( tab.getEditorText() );
385
    } catch( final Exception ex ) {
386
      error( ex );
387
    }
388
  }
389
390
  private void refreshActiveTab() {
391
    refreshSelectedTab( getActiveFileEditor() );
392
  }
393
394
  /**
395
   * Used to find text in the active file editor window.
396
   */
397
  private void find() {
398
    final TextField input = getFindTextField();
399
    getStatusBar().setGraphic( input );
400
    input.requestFocus();
401
  }
402
403
  public void findNext() {
404
    getActiveFileEditor().searchNext( getFindTextField().getText() );
405
  }
406
407
  /**
408
   * Returns the variable map of interpolated definitions.
409
   *
410
   * @return A map to help dereference variables.
411
   */
412
  private Map<String, String> getResolvedMap() {
413
    return mResolvedMap;
414
  }
415
416
  private void interpolateResolvedMap() {
417
    final Map<String, String> treeMap = getDefinitionPane().toMap();
418
    final Map<String, String> map = new HashMap<>( treeMap );
419
    MapInterpolator.interpolate( map );
420
421
    getResolvedMap().clear();
422
    getResolvedMap().putAll( map );
423
  }
424
425
  /**
426
   * Called when a definition source is opened.
427
   *
428
   * @param path Path to the definition source that was opened.
429
   */
430
  private void openDefinitions( final Path path ) {
431
    try {
432
      final DefinitionSource ds = createDefinitionSource( path );
433
      setDefinitionSource( ds );
434
      storeDefinitionSourceFilename( path );
435
436
      final Tooltip tooltipPath = new Tooltip( path.toString() );
437
      tooltipPath.setShowDelay( Duration.millis( 200 ) );
438
439
      final DefinitionPane pane = getDefinitionPane();
440
      pane.update( ds );
441
      pane.addTreeChangeHandler( mTreeHandler );
442
      pane.addKeyEventHandler( mDefinitionKeyHandler );
443
      pane.filenameProperty().setValue( path.getFileName().toString() );
444
      pane.setTooltip( tooltipPath );
445
446
      interpolateResolvedMap();
447
    } catch( final Exception e ) {
448
      error( e );
449
    }
450
  }
451
452
  private void exportDefinitions( final Path path ) {
453
    try {
454
      final DefinitionPane pane = getDefinitionPane();
455
      final TreeItem<String> root = pane.getTreeView().getRoot();
456
      final TreeItem<String> problemChild = pane.isTreeWellFormed();
457
458
      if( problemChild == null ) {
459
        getDefinitionSource().getTreeAdapter().export( root, path );
460
        getNotifier().clear();
461
      }
462
      else {
463
        final String msg = get( "yaml.error.tree.form",
464
                                problemChild.getValue() );
465
        getNotifier().notify( msg );
466
      }
467
    } catch( final Exception e ) {
468
      error( e );
469
    }
470
  }
471
472
  private Path getDefinitionPath() {
473
    final String source = getPreferences().get(
474
        PERSIST_DEFINITION_SOURCE, "" );
475
476
    return new File(
477
        source.isBlank()
478
            ? getSetting( "file.definition.default", "variables.yaml" )
479
            : source
480
    ).toPath();
481
  }
482
483
  private void restoreDefinitionPane() {
484
    openDefinitions( getDefinitionPath() );
485
  }
486
487
  private void storeDefinitionSourceFilename( final Path path ) {
488
    getPreferences().put( PERSIST_DEFINITION_SOURCE, path.toString() );
489
  }
490
491
  /**
492
   * Called when the last open tab is closed to clear the preview pane.
493
   */
494
  private void closeRemainingTab() {
495
    getPreviewPane().clear();
496
  }
497
498
  /**
499
   * Called when an exception occurs that warrants the user's attention.
500
   *
501
   * @param e The exception with a message that the user should know about.
502
   */
503
  private void error( final Exception e ) {
504
    getNotifier().notify( e );
505
  }
506
507
  //---- File actions -------------------------------------------------------
508
509
  /**
510
   * Called when an observable instance has changed. This is called by both the
511
   * snitch service and the notify service. The snitch service can be called for
512
   * different file types, including definition sources.
513
   *
514
   * @param observable The observed instance.
515
   * @param value      The noteworthy item.
516
   */
517
  @Override
518
  public void update( final Observable observable, final Object value ) {
519
    if( value != null ) {
520
      if( observable instanceof Snitch && value instanceof Path ) {
521
        updateSelectedTab();
522
      }
523
      else if( observable instanceof Notifier && value instanceof String ) {
524
        updateStatusBar( (String) value );
525
      }
526
    }
527
  }
528
529
  /**
530
   * Updates the status bar to show the given message.
531
   *
532
   * @param s The message to show in the status bar.
533
   */
534
  private void updateStatusBar( final String s ) {
535
    Platform.runLater(
536
        () -> {
537
          final int index = s.indexOf( '\n' );
538
          final String message = s.substring(
539
              0, index > 0 ? index : s.length() );
540
541
          getStatusBar().setText( message );
542
        }
543
    );
544
  }
545
546
  /**
547
   * Called when a file has been modified.
548
   */
549
  private void updateSelectedTab() {
550
    Platform.runLater(
551
        () -> {
552
          // Brute-force XSLT file reload by re-instantiating all processors.
553
          resetProcessors();
554
          refreshActiveTab();
555
        }
556
    );
557
  }
558
559
  /**
560
   * After resetting the processors, they will refresh anew to be up-to-date
561
   * with the files (text and definition) currently loaded into the editor.
562
   */
563
  private void resetProcessors() {
564
    getProcessors().clear();
565
  }
566
567
  //---- File actions -------------------------------------------------------
568
  private void fileNew() {
569
    getFileEditorPane().newEditor();
570
  }
571
572
  private void fileOpen() {
573
    getFileEditorPane().openFileDialog();
574
  }
575
576
  private void fileClose() {
577
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
578
  }
579
580
  private void fileCloseAll() {
581
    getFileEditorPane().closeAllEditors();
582
  }
583
584
  private void fileSave() {
585
    getFileEditorPane().saveEditor( getActiveFileEditor() );
586
  }
587
588
  private void fileSaveAs() {
589
    final FileEditorTab editor = getActiveFileEditor();
590
    getFileEditorPane().saveEditorAs( editor );
591
    getProcessors().remove( editor );
592
593
    try {
594
      refreshSelectedTab( editor );
595
    } catch( final Exception ex ) {
596
      getNotifier().notify( ex );
597
    }
598
  }
599
600
  private void fileSaveAll() {
601
    getFileEditorPane().saveAllEditors();
602
  }
603
604
  private void fileExit() {
605
    final Window window = getWindow();
606
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
607
  }
608
609
  //---- R menu actions
610
  private void rScript() {
611
    final String script = getPreferences().get( PERSIST_R_STARTUP, "" );
612
    final RScriptDialog dialog = new RScriptDialog(
613
        getWindow(), "Dialog.r.script.title", script );
614
    final Optional<String> result = dialog.showAndWait();
615
616
    result.ifPresent( this::putStartupScript );
617
  }
618
619
  private void rDirectory() {
620
    final TextInputDialog dialog = new TextInputDialog(
621
        getPreferences().get( PERSIST_R_DIRECTORY, USER_DIRECTORY )
622
    );
623
624
    dialog.setTitle( get( "Dialog.r.directory.title" ) );
625
    dialog.setHeaderText( getLiteral( "Dialog.r.directory.header" ) );
626
    dialog.setContentText( "Directory" );
627
628
    final Optional<String> result = dialog.showAndWait();
629
630
    result.ifPresent( this::putStartupDirectory );
631
  }
632
633
  /**
634
   * Stores the R startup script into the user preferences.
635
   */
636
  private void putStartupScript( final String script ) {
637
    putPreference( PERSIST_R_STARTUP, script );
638
  }
639
640
  /**
641
   * Stores the R bootstrap script directory into the user preferences.
642
   */
643
  private void putStartupDirectory( final String directory ) {
644
    putPreference( PERSIST_R_DIRECTORY, directory );
645
  }
646
647
  //---- Help actions -------------------------------------------------------
648
  private void helpAbout() {
649
    final Alert alert = new Alert( AlertType.INFORMATION );
650
    alert.setTitle( get( "Dialog.about.title" ) );
651
    alert.setHeaderText( get( "Dialog.about.header" ) );
652
    alert.setContentText( get( "Dialog.about.content" ) );
653
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
654
    alert.initOwner( getWindow() );
655
656
    alert.showAndWait();
657
  }
658
659
  //---- Convenience accessors ----------------------------------------------
660
  private float getFloat( final String key, final float defaultValue ) {
661
    return getPreferences().getFloat( key, defaultValue );
662
  }
663
664
  private Preferences getPreferences() {
665
    return getOptions().getState();
666
  }
667
668
  protected Scene getScene() {
669
    return mScene;
670
  }
671
672
  public Window getWindow() {
673
    return getScene().getWindow();
674
  }
675
676
  private MarkdownEditorPane getActiveEditor() {
677
    final EditorPane pane = getActiveFileEditor().getEditorPane();
678
679
    return pane instanceof MarkdownEditorPane
680
        ? (MarkdownEditorPane) pane
681
        : null;
682
  }
683
684
  private FileEditorTab getActiveFileEditor() {
685
    return getFileEditorPane().getActiveFileEditor();
686
  }
687
688
  //---- Member accessors ---------------------------------------------------
689
690
  private Map<FileEditorTab, Processor<String>> getProcessors() {
691
    return mProcessors;
692
  }
693
694
  private FileEditorTabPane getFileEditorPane() {
695
    if( this.fileEditorPane == null ) {
696
      this.fileEditorPane = createFileEditorPane();
697
    }
698
699
    return this.fileEditorPane;
700
  }
701
702
  private HTMLPreviewPane getPreviewPane() {
703
    return mPreviewPane;
704
  }
705
706
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
707
    assert definitionSource != null;
708
    mDefinitionSource = definitionSource;
709
  }
710
711
  private DefinitionSource getDefinitionSource() {
712
    return mDefinitionSource;
713
  }
714
715
  private DefinitionPane getDefinitionPane() {
716
    return mDefinitionPane;
717
  }
718
719
  private Options getOptions() {
720
    return mOptions;
721
  }
722
723
  private Snitch getSnitch() {
724
    return mSnitch;
725
  }
726
727
  private Notifier getNotifier() {
728
    return mNotifier;
729
  }
730
731
  private Text getLineNumberText() {
732
    return mLineNumberText;
733
  }
734
735
  private StatusBar getStatusBar() {
736
    return mStatusBar;
737
  }
738
739
  private TextField getFindTextField() {
740
    return mFindTextField;
741
  }
742
743
  //---- Member creators ----------------------------------------------------
744
745
  /**
746
   * Factory to create processors that are suited to different file types.
747
   *
748
   * @param tab The tab that is subjected to processing.
749
   * @return A processor suited to the file type specified by the tab's path.
750
   */
751
  private Processor<String> createProcessor( final FileEditorTab tab ) {
752
    return createProcessorFactory().createProcessor( tab );
753
  }
754
755
  private ProcessorFactory createProcessorFactory() {
756
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
757
  }
758
759
  private DefinitionSource createDefaultDefinitionSource() {
760
    return new YamlDefinitionSource( getDefinitionPath() );
761
  }
762
763
  private DefinitionSource createDefinitionSource( final Path path ) {
764
    try {
765
      return createDefinitionFactory().createDefinitionSource( path );
766
    } catch( final Exception ex ) {
767
      error( ex );
768
      return createDefaultDefinitionSource();
769
    }
770
  }
771
772
  private TextField createFindTextField() {
773
    return new TextField();
774
  }
775
776
  /**
777
   * Create an editor pane to hold file editor tabs.
778
   *
779
   * @return A new instance, never null.
780
   */
781
  private FileEditorTabPane createFileEditorPane() {
782
    return new FileEditorTabPane();
783
  }
784
785
  private DefinitionFactory createDefinitionFactory() {
786
    return new DefinitionFactory();
787
  }
788
789
  private StatusBar createStatusBar() {
790
    return new StatusBar();
791
  }
792
793
  private Scene createScene() {
794
    final SplitPane splitPane = new SplitPane(
795
        getDefinitionPane().getNode(),
796
        getFileEditorPane().getNode(),
797
        getPreviewPane().getNode() );
798
799
    splitPane.setDividerPositions(
800
        getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
801
        getFloat( K_PANE_SPLIT_EDITOR, .45f ),
802
        getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
803
804
    getDefinitionPane().prefHeightProperty().bind( splitPane.heightProperty() );
805
806
    final BorderPane borderPane = new BorderPane();
807
    borderPane.setPrefSize( 1024, 800 );
808
    borderPane.setTop( createMenuBar() );
809
    borderPane.setBottom( getStatusBar() );
810
    borderPane.setCenter( splitPane );
811
812
    final VBox statusBar = new VBox();
813
    statusBar.setAlignment( Pos.BASELINE_CENTER );
814
    statusBar.getChildren().add( getLineNumberText() );
815
    getStatusBar().getRightItems().add( statusBar );
810816
811817
    return new Scene( borderPane );
M src/main/java/com/scrivenvar/definition/DefinitionPane.java
2828
package com.scrivenvar.definition;
2929
30
import com.scrivenvar.AbstractPane;
31
import javafx.collections.ObservableList;
32
import javafx.event.Event;
33
import javafx.event.EventHandler;
34
import javafx.scene.Node;
35
import javafx.scene.control.*;
36
import javafx.scene.control.cell.TextFieldTreeCell;
37
import javafx.scene.input.KeyEvent;
38
import javafx.util.StringConverter;
39
40
import java.util.*;
41
42
import static com.scrivenvar.Messages.get;
43
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
44
45
/**
46
 * Provides the user interface that holdsa {@link TreeView}, which
47
 * allows users to interact with key/value pairs loaded from the
48
 * {@link DocumentParser} and adapted using a {@link TreeAdapter}.
49
 *
50
 * @author White Magic Software, Ltd.
51
 */
52
public final class DefinitionPane extends AbstractPane {
53
54
  /**
55
   * Trimmed off the end of a word to match a variable name.
56
   */
57
  private final static String TERMINALS = ":;,.!?-/\\¡¿";
58
59
  /**
60
   * Contains a view of the definitions.
61
   */
62
  private final TreeView<String> mTreeView = new TreeView<>();
63
64
  /**
65
   * Handlers for key press events.
66
   */
67
  private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
68
      = new HashSet<>();
69
70
  /**
71
   * Constructs a definition pane with a given tree view root.
72
   */
73
  public DefinitionPane() {
74
    final var treeView = getTreeView();
75
    treeView.setEditable( true );
76
    treeView.setCellFactory( cell -> createTreeCell() );
77
    treeView.setContextMenu( createContextMenu() );
78
    treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
79
    treeView.setShowRoot( false );
80
    getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
81
  }
82
83
  /**
84
   * Changes the root of the {@link TreeView} to the root of the
85
   * {@link TreeView} from the {@link DefinitionSource}.
86
   *
87
   * @param definitionSource Container for the hierarchy of key/value pairs
88
   *                         to replace the existing hierarchy.
89
   */
90
  public void update( final DefinitionSource definitionSource ) {
91
    assert definitionSource != null;
92
93
    final TreeAdapter treeAdapter = definitionSource.getTreeAdapter();
94
    final TreeItem<String> root = treeAdapter.adapt(
95
        get( "Pane.definition.node.root.title" )
96
    );
97
98
    getTreeView().setRoot( root );
99
  }
100
101
  public Map<String, String> toMap() {
102
    return TreeItemAdapter.toMap( getTreeView().getRoot() );
103
  }
104
105
  /**
106
   * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
107
   * is modified. The modifications include: item value changes, item additions,
108
   * and item removals.
109
   * <p>
110
   * Safe to call multiple times; if a handler is already registered, the
111
   * old handler is used.
112
   * </p>
113
   *
114
   * @param handler The handler to call whenever any {@link TreeItem} changes.
115
   */
116
  public void addTreeChangeHandler(
117
      final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
118
    final TreeItem<String> root = getTreeView().getRoot();
119
    root.addEventHandler( TreeItem.valueChangedEvent(), handler );
120
    root.addEventHandler( TreeItem.childrenModificationEvent(), handler );
121
  }
122
123
  public void addKeyEventHandler(
124
      final EventHandler<? super KeyEvent> handler ) {
125
    getKeyEventHandlers().add( handler );
126
  }
127
128
  /**
129
   * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
130
   * well-formed for export. A tree is considered well-formed if the following
131
   * conditions are met:
132
   *
133
   * <ul>
134
   *   <li>The root node contains at least one child node having a leaf.</li>
135
   *   <li>There are no leaf nodes with sibling leaf nodes.</li>
136
   * </ul>
137
   *
138
   * @return {@code null} if the document is well-formed, otherwise the
139
   * problematic child {@link TreeItem}.
140
   */
141
  public TreeItem<String> isTreeWellFormed() {
142
    final var root = getTreeView().getRoot();
143
144
    for( final var child : root.getChildren() ) {
145
      final var problemChild = isWellFormed( child );
146
147
      if( child.isLeaf() || problemChild != null ) {
148
        return problemChild;
149
      }
150
    }
151
152
    return null;
153
  }
154
155
  /**
156
   * Determines whether the document is well-formed by ensuring that
157
   * child branches do not contain multiple leaves.
158
   *
159
   * @param item The sub-tree to check for well-formedness.
160
   * @return {@code null} when the tree is well-formed, otherwise the
161
   * problematic {@link TreeItem}.
162
   */
163
  private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
164
    int childLeafs = 0;
165
    int childBranches = 0;
166
167
    for( final TreeItem<String> child : item.getChildren() ) {
168
      if( child.isLeaf() ) {
169
        childLeafs++;
170
      }
171
      else {
172
        childBranches++;
173
      }
174
175
      final var problemChild = isWellFormed( child );
176
177
      if( problemChild != null ) {
178
        return problemChild;
179
      }
180
    }
181
182
    return ((childBranches > 0 && childLeafs == 0) ||
183
        (childBranches == 0 && childLeafs <= 1)) ? null : item;
184
  }
185
186
  /**
187
   * Returns the leaf that matches the given value. If the value is terminally
188
   * punctuated, the punctuation is removed if no match was found.
189
   *
190
   * @param value    The value to find, never null.
191
   * @param findMode Defines how to match words.
192
   * @return The leaf that contains the given value, or null if neither the
193
   * original value nor the terminally-trimmed value was found.
194
   */
195
  public VariableTreeItem<String> findLeaf(
196
      final String value, final FindMode findMode ) {
197
    final VariableTreeItem<String> root = getTreeRoot();
198
    final VariableTreeItem<String> leaf = root.findLeaf( value, findMode );
199
200
    return leaf == null
201
        ? root.findLeaf( rtrimTerminalPunctuation( value ) )
202
        : leaf;
203
  }
204
205
  /**
206
   * Removes punctuation from the end of a string.
207
   *
208
   * @param s The string to trim, never null.
209
   * @return The string trimmed of all terminal characters from the end
210
   */
211
  private String rtrimTerminalPunctuation( final String s ) {
212
    assert s != null;
213
    int index = s.length() - 1;
214
215
    while( index > 0 && (TERMINALS.indexOf( s.charAt( index ) ) >= 0) ) {
216
      index--;
217
    }
218
219
    return s.substring( 0, index );
220
  }
221
222
  /**
223
   * Expands the node to the root, recursively.
224
   *
225
   * @param <T>  The type of tree item to expand (usually String).
226
   * @param node The node to expand.
227
   */
228
  public <T> void expand( final TreeItem<T> node ) {
229
    if( node != null ) {
230
      expand( node.getParent() );
231
232
      if( !node.isLeaf() ) {
233
        node.setExpanded( true );
234
      }
235
    }
236
  }
237
238
  public void select( final TreeItem<String> item ) {
239
    getSelectionModel().clearSelection();
240
    getSelectionModel().select( getTreeView().getRow( item ) );
241
  }
242
243
  /**
244
   * Collapses the tree, recursively.
245
   */
246
  public void collapse() {
247
    collapse( getTreeRoot().getChildren() );
248
  }
249
250
  /**
251
   * Collapses the tree, recursively.
252
   *
253
   * @param <T>   The type of tree item to expand (usually String).
254
   * @param nodes The nodes to collapse.
255
   */
256
  private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
257
    for( final TreeItem<T> node : nodes ) {
258
      node.setExpanded( false );
259
      collapse( node.getChildren() );
260
    }
261
  }
262
263
  /**
264
   * @return {@code true} when the user is editing a {@link TreeItem}.
265
   */
266
  private boolean isEditingTreeItem() {
267
    return getTreeView().editingItemProperty().getValue() != null;
268
  }
269
270
  /**
271
   * Changes to edit mode for the selected item.
272
   */
273
  private void editSelectedItem() {
274
    getTreeView().edit( getSelectedItem() );
275
  }
276
277
  /**
278
   * Removes all selected items from the {@link TreeView}.
279
   */
280
  private void deleteSelectedItems() {
281
    for( final TreeItem<String> item : getSelectedItems() ) {
282
      final TreeItem<String> parent = item.getParent();
283
284
      if( parent != null ) {
285
        parent.getChildren().remove( item );
286
      }
287
    }
288
  }
289
290
  /**
291
   * Deletes the selected item.
292
   */
293
  private void deleteSelectedItem() {
294
    final TreeItem<String> c = getSelectedItem();
295
    getSiblings( c ).remove( c );
296
  }
297
298
  /**
299
   * Adds a new item under the selected item (or root if nothing is selected).
300
   * There are a few conditions to consider: when adding to the root,
301
   * when adding to a leaf, and when adding to a non-leaf. Items added to the
302
   * root must contain two items: a key and a value.
303
   */
304
  private void addItem() {
305
    final TreeItem<String> value = createTreeItem();
306
    getSelectedItem().getChildren().add( value );
307
    expand( value );
308
    select( value );
309
  }
310
311
  private ContextMenu createContextMenu() {
312
    final ContextMenu menu = new ContextMenu();
313
    final ObservableList<MenuItem> items = menu.getItems();
314
315
    addMenuItem( items, "Definition.menu.create" )
316
        .setOnAction( e -> addItem() );
317
318
    addMenuItem( items, "Definition.menu.rename" )
319
        .setOnAction( e -> editSelectedItem() );
320
321
    addMenuItem( items, "Definition.menu.remove" )
322
        .setOnAction( e -> deleteSelectedItem() );
323
324
    return menu;
325
  }
326
327
  /**
328
   * Executes hot-keys for edits to the definition tree.
329
   *
330
   * @param event Contains the key code of the key that was pressed.
331
   */
332
  private void keyEventFilter( final KeyEvent event ) {
333
    if( !isEditingTreeItem() ) {
334
      switch( event.getCode() ) {
335
        case ENTER:
336
          expand( getSelectedItem() );
337
          event.consume();
338
          break;
339
340
        case DELETE:
341
          deleteSelectedItems();
342
          break;
343
344
        case INSERT:
345
          addItem();
346
          break;
347
348
        case R:
349
          if( event.isControlDown() ) {
350
            editSelectedItem();
351
          }
352
353
          break;
354
      }
355
356
      for( final var handler : getKeyEventHandlers() ) {
357
        handler.handle( event );
358
      }
359
    }
360
  }
361
362
  /**
363
   * Adds a menu item to a list of menu items.
364
   *
365
   * @param items    The list of menu items to append to.
366
   * @param labelKey The resource bundle key name for the menu item's label.
367
   * @return The menu item added to the list of menu items.
368
   */
369
  private MenuItem addMenuItem(
370
      final List<MenuItem> items, final String labelKey ) {
371
    final MenuItem menuItem = createMenuItem( labelKey );
372
    items.add( menuItem );
373
    return menuItem;
374
  }
375
376
  private MenuItem createMenuItem( final String labelKey ) {
377
    return new MenuItem( get( labelKey ) );
378
  }
379
380
  private VariableTreeItem<String> createTreeItem() {
381
    return new VariableTreeItem<>( get( "Definition.menu.add.default" ) );
382
  }
383
384
  private TreeCell<String> createTreeCell() {
385
    return new TextFieldTreeCell<>(
386
        createStringConverter() ) {
387
      @Override
388
      public void commitEdit( final String newValue ) {
389
        super.commitEdit( newValue );
390
        select( getTreeItem() );
391
        requestFocus();
392
      }
393
    };
394
  }
395
396
  @Override
397
  public void requestFocus() {
398
    super.requestFocus();
399
    getTreeView().requestFocus();
400
  }
401
402
  private StringConverter<String> createStringConverter() {
403
    return new StringConverter<>() {
404
      @Override
405
      public String toString( final String object ) {
406
        return object == null ? "" : object;
407
      }
408
409
      @Override
410
      public String fromString( final String string ) {
411
        return string == null ? "" : string;
412
      }
413
    };
414
  }
415
416
  /**
417
   * Returns the tree view that contains the definition hierarchy.
418
   *
419
   * @return A non-null instance.
420
   */
421
  public TreeView<String> getTreeView() {
422
    return mTreeView;
423
  }
424
425
  /**
426
   * Returns the root node to the tree view.
427
   *
428
   * @return getTreeView()
429
   */
430
  public Node getNode() {
431
    return getTreeView();
30
import javafx.beans.property.SimpleStringProperty;
31
import javafx.beans.property.StringProperty;
32
import javafx.collections.ObservableList;
33
import javafx.event.Event;
34
import javafx.event.EventHandler;
35
import javafx.scene.Node;
36
import javafx.scene.control.*;
37
import javafx.scene.control.cell.TextFieldTreeCell;
38
import javafx.scene.input.KeyEvent;
39
import javafx.util.StringConverter;
40
41
import java.util.*;
42
43
import static com.scrivenvar.Messages.get;
44
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
45
46
/**
47
 * Provides the user interface that holdsa {@link TreeView}, which
48
 * allows users to interact with key/value pairs loaded from the
49
 * {@link DocumentParser} and adapted using a {@link TreeAdapter}.
50
 *
51
 * @author White Magic Software, Ltd.
52
 */
53
public final class DefinitionPane extends TitledPane {
54
55
  /**
56
   * Trimmed off the end of a word to match a variable name.
57
   */
58
  private final static String TERMINALS = ":;,.!?-/\\¡¿";
59
60
  /**
61
   * Contains a view of the definitions.
62
   */
63
  private final TreeView<String> mTreeView = new TreeView<>();
64
65
  /**
66
   * Handlers for key press events.
67
   */
68
  private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
69
      = new HashSet<>();
70
71
  /**
72
   * Definition file name shown in the title of the pane.
73
   */
74
  private final StringProperty mFilename = new SimpleStringProperty();
75
76
  /**
77
   * Constructs a definition pane with a given tree view root.
78
   */
79
  public DefinitionPane() {
80
    final var treeView = getTreeView();
81
    treeView.setEditable( true );
82
    treeView.setCellFactory( cell -> createTreeCell() );
83
    treeView.setContextMenu( createContextMenu() );
84
    treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
85
    treeView.setShowRoot( false );
86
    getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
87
88
    textProperty().bind( mFilename );
89
90
    setContent( treeView );
91
    setCollapsible( false );
92
  }
93
94
  /**
95
   * Changes the root of the {@link TreeView} to the root of the
96
   * {@link TreeView} from the {@link DefinitionSource}.
97
   *
98
   * @param definitionSource Container for the hierarchy of key/value pairs
99
   *                         to replace the existing hierarchy.
100
   */
101
  public void update( final DefinitionSource definitionSource ) {
102
    assert definitionSource != null;
103
104
    final TreeAdapter treeAdapter = definitionSource.getTreeAdapter();
105
    final TreeItem<String> root = treeAdapter.adapt(
106
        get( "Pane.definition.node.root.title" )
107
    );
108
109
    getTreeView().setRoot( root );
110
  }
111
112
  public Map<String, String> toMap() {
113
    return TreeItemAdapter.toMap( getTreeView().getRoot() );
114
  }
115
116
  /**
117
   * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
118
   * is modified. The modifications include: item value changes, item additions,
119
   * and item removals.
120
   * <p>
121
   * Safe to call multiple times; if a handler is already registered, the
122
   * old handler is used.
123
   * </p>
124
   *
125
   * @param handler The handler to call whenever any {@link TreeItem} changes.
126
   */
127
  public void addTreeChangeHandler(
128
      final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
129
    final TreeItem<String> root = getTreeView().getRoot();
130
    root.addEventHandler( TreeItem.valueChangedEvent(), handler );
131
    root.addEventHandler( TreeItem.childrenModificationEvent(), handler );
132
  }
133
134
  public void addKeyEventHandler(
135
      final EventHandler<? super KeyEvent> handler ) {
136
    getKeyEventHandlers().add( handler );
137
  }
138
139
  /**
140
   * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
141
   * well-formed for export. A tree is considered well-formed if the following
142
   * conditions are met:
143
   *
144
   * <ul>
145
   *   <li>The root node contains at least one child node having a leaf.</li>
146
   *   <li>There are no leaf nodes with sibling leaf nodes.</li>
147
   * </ul>
148
   *
149
   * @return {@code null} if the document is well-formed, otherwise the
150
   * problematic child {@link TreeItem}.
151
   */
152
  public TreeItem<String> isTreeWellFormed() {
153
    final var root = getTreeView().getRoot();
154
155
    for( final var child : root.getChildren() ) {
156
      final var problemChild = isWellFormed( child );
157
158
      if( child.isLeaf() || problemChild != null ) {
159
        return problemChild;
160
      }
161
    }
162
163
    return null;
164
  }
165
166
  /**
167
   * Determines whether the document is well-formed by ensuring that
168
   * child branches do not contain multiple leaves.
169
   *
170
   * @param item The sub-tree to check for well-formedness.
171
   * @return {@code null} when the tree is well-formed, otherwise the
172
   * problematic {@link TreeItem}.
173
   */
174
  private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
175
    int childLeafs = 0;
176
    int childBranches = 0;
177
178
    for( final TreeItem<String> child : item.getChildren() ) {
179
      if( child.isLeaf() ) {
180
        childLeafs++;
181
      }
182
      else {
183
        childBranches++;
184
      }
185
186
      final var problemChild = isWellFormed( child );
187
188
      if( problemChild != null ) {
189
        return problemChild;
190
      }
191
    }
192
193
    return ((childBranches > 0 && childLeafs == 0) ||
194
        (childBranches == 0 && childLeafs <= 1)) ? null : item;
195
  }
196
197
  /**
198
   * Returns the leaf that matches the given value. If the value is terminally
199
   * punctuated, the punctuation is removed if no match was found.
200
   *
201
   * @param value    The value to find, never null.
202
   * @param findMode Defines how to match words.
203
   * @return The leaf that contains the given value, or null if neither the
204
   * original value nor the terminally-trimmed value was found.
205
   */
206
  public VariableTreeItem<String> findLeaf(
207
      final String value, final FindMode findMode ) {
208
    final VariableTreeItem<String> root = getTreeRoot();
209
    final VariableTreeItem<String> leaf = root.findLeaf( value, findMode );
210
211
    return leaf == null
212
        ? root.findLeaf( rtrimTerminalPunctuation( value ) )
213
        : leaf;
214
  }
215
216
  /**
217
   * Removes punctuation from the end of a string.
218
   *
219
   * @param s The string to trim, never null.
220
   * @return The string trimmed of all terminal characters from the end
221
   */
222
  private String rtrimTerminalPunctuation( final String s ) {
223
    assert s != null;
224
    int index = s.length() - 1;
225
226
    while( index > 0 && (TERMINALS.indexOf( s.charAt( index ) ) >= 0) ) {
227
      index--;
228
    }
229
230
    return s.substring( 0, index );
231
  }
232
233
  /**
234
   * Expands the node to the root, recursively.
235
   *
236
   * @param <T>  The type of tree item to expand (usually String).
237
   * @param node The node to expand.
238
   */
239
  public <T> void expand( final TreeItem<T> node ) {
240
    if( node != null ) {
241
      expand( node.getParent() );
242
243
      if( !node.isLeaf() ) {
244
        node.setExpanded( true );
245
      }
246
    }
247
  }
248
249
  public void select( final TreeItem<String> item ) {
250
    getSelectionModel().clearSelection();
251
    getSelectionModel().select( getTreeView().getRow( item ) );
252
  }
253
254
  /**
255
   * Collapses the tree, recursively.
256
   */
257
  public void collapse() {
258
    collapse( getTreeRoot().getChildren() );
259
  }
260
261
  /**
262
   * Collapses the tree, recursively.
263
   *
264
   * @param <T>   The type of tree item to expand (usually String).
265
   * @param nodes The nodes to collapse.
266
   */
267
  private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
268
    for( final TreeItem<T> node : nodes ) {
269
      node.setExpanded( false );
270
      collapse( node.getChildren() );
271
    }
272
  }
273
274
  /**
275
   * @return {@code true} when the user is editing a {@link TreeItem}.
276
   */
277
  private boolean isEditingTreeItem() {
278
    return getTreeView().editingItemProperty().getValue() != null;
279
  }
280
281
  /**
282
   * Changes to edit mode for the selected item.
283
   */
284
  private void editSelectedItem() {
285
    getTreeView().edit( getSelectedItem() );
286
  }
287
288
  /**
289
   * Removes all selected items from the {@link TreeView}.
290
   */
291
  private void deleteSelectedItems() {
292
    for( final TreeItem<String> item : getSelectedItems() ) {
293
      final TreeItem<String> parent = item.getParent();
294
295
      if( parent != null ) {
296
        parent.getChildren().remove( item );
297
      }
298
    }
299
  }
300
301
  /**
302
   * Deletes the selected item.
303
   */
304
  private void deleteSelectedItem() {
305
    final TreeItem<String> c = getSelectedItem();
306
    getSiblings( c ).remove( c );
307
  }
308
309
  /**
310
   * Adds a new item under the selected item (or root if nothing is selected).
311
   * There are a few conditions to consider: when adding to the root,
312
   * when adding to a leaf, and when adding to a non-leaf. Items added to the
313
   * root must contain two items: a key and a value.
314
   */
315
  private void addItem() {
316
    final TreeItem<String> value = createTreeItem();
317
    getSelectedItem().getChildren().add( value );
318
    expand( value );
319
    select( value );
320
  }
321
322
  private ContextMenu createContextMenu() {
323
    final ContextMenu menu = new ContextMenu();
324
    final ObservableList<MenuItem> items = menu.getItems();
325
326
    addMenuItem( items, "Definition.menu.create" )
327
        .setOnAction( e -> addItem() );
328
329
    addMenuItem( items, "Definition.menu.rename" )
330
        .setOnAction( e -> editSelectedItem() );
331
332
    addMenuItem( items, "Definition.menu.remove" )
333
        .setOnAction( e -> deleteSelectedItem() );
334
335
    return menu;
336
  }
337
338
  /**
339
   * Executes hot-keys for edits to the definition tree.
340
   *
341
   * @param event Contains the key code of the key that was pressed.
342
   */
343
  private void keyEventFilter( final KeyEvent event ) {
344
    if( !isEditingTreeItem() ) {
345
      switch( event.getCode() ) {
346
        case ENTER:
347
          expand( getSelectedItem() );
348
          event.consume();
349
          break;
350
351
        case DELETE:
352
          deleteSelectedItems();
353
          break;
354
355
        case INSERT:
356
          addItem();
357
          break;
358
359
        case R:
360
          if( event.isControlDown() ) {
361
            editSelectedItem();
362
          }
363
364
          break;
365
      }
366
367
      for( final var handler : getKeyEventHandlers() ) {
368
        handler.handle( event );
369
      }
370
    }
371
  }
372
373
  /**
374
   * Adds a menu item to a list of menu items.
375
   *
376
   * @param items    The list of menu items to append to.
377
   * @param labelKey The resource bundle key name for the menu item's label.
378
   * @return The menu item added to the list of menu items.
379
   */
380
  private MenuItem addMenuItem(
381
      final List<MenuItem> items, final String labelKey ) {
382
    final MenuItem menuItem = createMenuItem( labelKey );
383
    items.add( menuItem );
384
    return menuItem;
385
  }
386
387
  private MenuItem createMenuItem( final String labelKey ) {
388
    return new MenuItem( get( labelKey ) );
389
  }
390
391
  private VariableTreeItem<String> createTreeItem() {
392
    return new VariableTreeItem<>( get( "Definition.menu.add.default" ) );
393
  }
394
395
  private TreeCell<String> createTreeCell() {
396
    return new TextFieldTreeCell<>(
397
        createStringConverter() ) {
398
      @Override
399
      public void commitEdit( final String newValue ) {
400
        super.commitEdit( newValue );
401
        select( getTreeItem() );
402
        requestFocus();
403
      }
404
    };
405
  }
406
407
  @Override
408
  public void requestFocus() {
409
    super.requestFocus();
410
    getTreeView().requestFocus();
411
  }
412
413
  private StringConverter<String> createStringConverter() {
414
    return new StringConverter<>() {
415
      @Override
416
      public String toString( final String object ) {
417
        return object == null ? "" : object;
418
      }
419
420
      @Override
421
      public String fromString( final String string ) {
422
        return string == null ? "" : string;
423
      }
424
    };
425
  }
426
427
  /**
428
   * Returns the tree view that contains the definition hierarchy.
429
   *
430
   * @return A non-null instance.
431
   */
432
  public TreeView<String> getTreeView() {
433
    return mTreeView;
434
  }
435
436
  /**
437
   * Returns this pane.
438
   *
439
   * @return this
440
   */
441
  public Node getNode() {
442
    return this;
443
  }
444
445
  /**
446
   * Returns the property used to set the title of the pane: the file name.
447
   *
448
   * @return A non-null property used for showing the definition file name.
449
   */
450
  public StringProperty filenameProperty() {
451
    return mFilename;
432452
  }
433453