Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M docs/README.md
33
See the following documents for more information:
44
5
* [definitions.md](definitions.md) -- Definitions and interpolation
5
* [variables.md](variables.md) -- Variable definitions and interpolation
66
* [r.md](r.md) -- Call R functions within R Markdown documents
77
* [svg.md](svg.md) -- Fix known issues with displaying SVG files
D docs/definitions.md
1
# Introduction
2
3
This document describes how to use the application.
4
5
# Variable definitions
6
7
Variable definitions provide a way to insert key names having associated values into a document. The variable names and values are declared inside an external file using the [YAML](http://www.yaml.org/) file format. Simply put, variables are written in the file as follows:
8
9
```
10
key: value
11
```
12
13
Any number of variables can be defined, in any order:
14
15
```
16
key_1: Value 1
17
key_2: Value 2
18
```
19
20
Variables can reference other variables by bookending the key name within symbols:
21
22
```
23
key: Value
24
key_1: {{key}} 1
25
key_2: {{key}} 2
26
```
27
28
Variables can use a nested structure to help group related information:
29
30
```
31
novel:
32
  title: Book Title
33
  author: Author Name
34
  isbn: 978-3-16-148410-0
35
```
36
37
Use a period to reference nested keys, such as:
38
39
```
40
novel:
41
  author: Author Name
42
copyright:
43
  owner: {{novel.author}}
44
```
45
46
Save the variable definitions in a file having an extension of `.yaml` or `.yml`.
47
48
# Document editing
49
50
The application's purpose is to completely separate the document's content from its presentation. To achieve this, documents are composed using a [plain text](http://spec.commonmark.org/0.28/) format.
51
52
## Create document
53
54
Start a new document as follows:
55
56
1. Start the application.
57
1. Click **File → New** to create an empty document to edit.
58
1. Click **File → Open** to open a variable definition file.
59
1. Change **Source Files** to **Definition Files** to list definition files.
60
1. Browse to and select a file saved with a `.yaml` or `.yml` extension.
61
1. Click **Open**.
62
63
The variable definitions appear in the variable definition pane under the heading of **Definitions**.
64
65
## Edit document
66
67
Edit the document as normal. Notice how the preview pane updates as new content is added. The toolbar shows various icons that perform different formatting operations. Try them to see how they appear in the preview pane. Other operations not shown on the toolbar include:
68
69
* Struck text (enclose the words within `~~` and `~~`)
70
* Horizontal rule (use `---` on an otherwise empty line).
71
72
The preview pane shows one way to interpret and format the document, but many other presentations are possible.
73
74
## Insert variable
75
76
Let's assume that the variable definitions loaded into the application include:
77
78
```
79
novel:
80
  title: Diary of {{novel.author}}
81
  author: Anne Frank
82
```
83
84
To reference a variable, type in the key name enclosed within double braces, such as:
85
86
```
87
The novel "{{novel.title}}" is one of the most widely read books in the world.
88
```
89
90
The preview pane shows:
91
92
> The novel "Diary of Anne Frank" is one of the most widely read books in the world.
93
94
As it is laborious to type in variable names, it is possible to inject the variable name using autocomplete. Accomplish this as follows:
95
96
1. Create a new file.
97
1. Type in a partial variable value, such as **Dia**.
98
1. Press `Ctrl+Space` (hold down the `Control` key and tap the spacebar).
99
100
The editor shows:
101
102
```
103
{{novel.title}}
104
```
105
106
The preview pane shows:
107
108
```
109
Diary of Anne Frank
110
```
111
112
The variable name is inserted into the document and the preview pane shows the variable's value.
113
1141
M docs/r.md
110110
working directory where the R engine searches for source files.
111111
112
# YAML definitions
112
# YAML variable definitions
113113
114114
To see how variable definitions work in R, try the following:
...
123123
1. Save the file as `definitions.yaml`.
124124
1. Click **File → Open**.
125
1. Set **Source Files** to **Definition Files**.
125
1. Set **Source Files** to **Variable Files**.
126126
1. Select `definitions.yaml`.
127127
1. Click **Open**.
...
142142
```
143143
144
This is because the application inserts definition reference names based
144
This is because the application inserts variable reference names based
145145
on the type of file being edited. By default, the R engine does not have
146146
a function named `x` defined.
...
173173
174174
The `x` function attempts to evaluate the expression defined by the YAML
175
variable. This means that the YAML definitions can also include expressions
175
variable. This means that the YAML variables can also include expressions
176176
that R is capable of evaluating.
177177
A docs/variables.md
1
# Introduction
2
3
This document describes how to use the application.
4
5
# Variable definitions
6
7
Variable definitions provide a way to insert key names having associated values into a document. The variable names and values are declared inside an external file using the [YAML](http://www.yaml.org/) file format. Simply put, variables are written in the file as follows:
8
9
```
10
key: value
11
```
12
13
Any number of variables can be defined, in any order:
14
15
```
16
key_1: Value 1
17
key_2: Value 2
18
```
19
20
Variables can reference other variables by bookending the key name within symbols:
21
22
```
23
key: Value
24
key_1: {{key}} 1
25
key_2: {{key}} 2
26
```
27
28
Variables can use a nested structure to help group related information:
29
30
```
31
novel:
32
  title: Book Title
33
  author: Author Name
34
  isbn: 978-3-16-148410-0
35
```
36
37
Use a period to reference nested keys, such as:
38
39
```
40
novel:
41
  author: Author Name
42
copyright:
43
  owner: {{novel.author}}
44
```
45
46
Save the variable definitions in a file having an extension of `.yaml` or `.yml`.
47
48
# Document editing
49
50
The application's purpose is to completely separate the document's content from its presentation. To achieve this, documents are composed using a [plain text](http://spec.commonmark.org/0.28/) format.
51
52
## Create document
53
54
Start a new document as follows:
55
56
1. Start the application.
57
1. Click **File → New** to create an empty document to edit.
58
1. Click **File → Open** to open a variable definition file.
59
1. Change **Source Files** to **Variable Files** to list variable definition files.
60
1. Browse to and select a file saved with a `.yaml` or `.yml` extension.
61
1. Click **Open**.
62
63
The variable definitions appear in the variable definition pane under the heading of **Variables**.
64
65
## Edit document
66
67
Edit the document as normal. Notice how the preview pane updates as new content is added. The toolbar shows various icons that perform different formatting operations. Try them to see how they appear in the preview pane. Other operations not shown on the toolbar include:
68
69
* Struck text (enclose the words within `~~` and `~~`)
70
* Horizontal rule (use `---` on an otherwise empty line).
71
72
The preview pane shows one way to interpret and format the document, but many other presentations are possible.
73
74
## Insert variable
75
76
Let's assume that the variable definitions loaded into the application include:
77
78
```
79
novel:
80
  title: Diary of {{novel.author}}
81
  author: Anne Frank
82
```
83
84
To reference a variable, type in the key name enclosed within double braces, such as:
85
86
```
87
The novel "{{novel.title}}" is one of the most widely read books in the world.
88
```
89
90
The preview pane shows:
91
92
> The novel "Diary of Anne Frank" is one of the most widely read books in the world.
93
94
As it is laborious to type in variable names, it is possible to inject the variable name using autocomplete. Accomplish this as follows:
95
96
1. Create a new file.
97
1. Type in a partial variable value, such as **Dia**.
98
1. Press `Ctrl+Space` (hold down the `Control` key and tap the spacebar).
99
100
The editor shows:
101
102
```
103
{{novel.title}}
104
```
105
106
The preview pane shows:
107
108
```
109
Diary of Anne Frank
110
```
111
112
The variable name is inserted into the document and the preview pane shows the variable's value.
113
1114
M src/main/java/com/keenwrite/MainPane.java
114114
   * Renders the actively selected plain text editor tab.
115115
   */
116
  private final HtmlPreview mHtmlPreview;
117
118
  /**
119
   * Provides an interactive document outline.
120
   */
121
  private final DocumentOutline mDocumentOutline = new DocumentOutline();
122
123
  /**
124
   * Changing the active editor fires the value changed event. This allows
125
   * refreshes to happen when external definitions are modified and need to
126
   * trigger the processing chain.
127
   */
128
  private final ObjectProperty<TextEditor> mActiveTextEditor =
129
    createActiveTextEditor();
130
131
  /**
132
   * Changing the active definition editor fires the value changed event. This
133
   * allows refreshes to happen when external definitions are modified and need
134
   * to trigger the processing chain.
135
   */
136
  private final ObjectProperty<TextDefinition> mActiveDefinitionEditor =
137
    createActiveDefinitionEditor( mActiveTextEditor );
138
139
  /**
140
   * Tracks the number of detached tab panels opened into their own windows,
141
   * which allows unique identification of subordinate windows by their title.
142
   * It is doubtful more than 128 windows, much less 256, will be created.
143
   */
144
  private byte mWindowCount;
145
146
  /**
147
   * Called when the definition data is changed.
148
   */
149
  private final EventHandler<TreeModificationEvent<Event>> mTreeHandler =
150
    event -> {
151
      final var editor = mActiveDefinitionEditor.get();
152
153
      resolve( editor );
154
      process( getActiveTextEditor() );
155
      save( editor );
156
    };
157
158
  /**
159
   * Adds all content panels to the main user interface. This will load the
160
   * configuration settings from the workspace to reproduce the settings from
161
   * a previous session.
162
   */
163
  public MainPane( final Workspace workspace ) {
164
    mWorkspace = workspace;
165
    mHtmlPreview = new HtmlPreview( workspace );
166
167
    open( bin( getRecentFiles() ) );
168
    viewPreview();
169
    setDividerPositions( calculateDividerPositions() );
170
171
    // Once the main scene's window regains focus, update the active definition
172
    // editor to the currently selected tab.
173
    runLater(
174
      () -> getWindow().setOnCloseRequest( ( event ) -> {
175
        // Order matters here. We want to close all the tabs to ensure each
176
        // is saved, but after they are closed, the workspace should still
177
        // retain the list of files that were open. If this line came after
178
        // closing, then restarting the application would list no files.
179
        mWorkspace.save();
180
181
        if( closeAll() ) {
182
          Platform.exit();
183
          System.exit( 0 );
184
        }
185
        else {
186
          event.consume();
187
        }
188
      } )
189
    );
190
191
    register( this );
192
  }
193
194
  @Subscribe
195
  public void handle( final TextEditorFocusEvent event ) {
196
    mActiveTextEditor.set( event.get() );
197
  }
198
199
  @Subscribe
200
  public void handle( final TextDefinitionFocusEvent event ) {
201
    mActiveDefinitionEditor.set( event.get() );
202
  }
203
204
  /**
205
   * Typically called when a file name is clicked in the {@link HtmlPanel}.
206
   *
207
   * @param event The event to process, must contain a valid file reference.
208
   */
209
  @Subscribe
210
  public void handle( final FileOpenEvent event ) {
211
    final File eventFile;
212
    final var eventUri = event.getUri();
213
214
    if( eventUri.isAbsolute() ) {
215
      eventFile = new File( eventUri.getPath() );
216
    }
217
    else {
218
      final var activeFile = getActiveTextEditor().getFile();
219
      final var parent = activeFile.getParentFile();
220
221
      if( parent == null ) {
222
        clue( new FileNotFoundException( eventUri.getPath() ) );
223
        return;
224
      }
225
      else {
226
        final var parentPath = parent.getAbsolutePath();
227
        eventFile = Path.of( parentPath, eventUri.getPath() ).toFile();
228
      }
229
    }
230
231
    runLater( () -> open( eventFile ) );
232
  }
233
234
  @Subscribe
235
  public void handle( final CaretNavigationEvent event ) {
236
    runLater( () -> {
237
      final var textArea = getActiveTextEditor().getTextArea();
238
      textArea.moveTo( event.getOffset() );
239
      textArea.requestFollowCaret();
240
      textArea.requestFocus();
241
    } );
242
  }
243
244
  /**
245
   * TODO: Load divider positions from exported settings, see bin() comment.
246
   */
247
  private double[] calculateDividerPositions() {
248
    final var ratio = 100f / getItems().size() / 100;
249
    final var positions = getDividerPositions();
250
251
    for( int i = 0; i < positions.length; i++ ) {
252
      positions[ i ] = ratio * i;
253
    }
254
255
    return positions;
256
  }
257
258
  /**
259
   * Opens all the files into the application, provided the paths are unique.
260
   * This may only be called for any type of files that a user can edit
261
   * (i.e., update and persist), such as definitions and text files.
262
   *
263
   * @param files The list of files to open.
264
   */
265
  public void open( final List<File> files ) {
266
    files.forEach( this::open );
267
  }
268
269
  /**
270
   * This opens the given file. Since the preview pane is not a file that
271
   * can be opened, it is safe to add a listener to the detachable pane.
272
   *
273
   * @param file The file to open.
274
   */
275
  private void open( final File file ) {
276
    final var tab = createTab( file );
277
    final var node = tab.getContent();
278
    final var mediaType = MediaType.valueFrom( file );
279
    final var tabPane = obtainTabPane( mediaType );
280
281
    tab.setTooltip( createTooltip( file ) );
282
    tabPane.setFocusTraversable( false );
283
    tabPane.setTabClosingPolicy( ALL_TABS );
284
    tabPane.getTabs().add( tab );
285
286
    // Attach the tab scene factory for new tab panes.
287
    if( !getItems().contains( tabPane ) ) {
288
      addTabPane(
289
        node instanceof TextDefinition ? 0 : getItems().size(), tabPane
290
      );
291
    }
292
293
    getRecentFiles().add( file.getAbsolutePath() );
294
  }
295
296
  /**
297
   * Opens a new text editor document using the default document file name.
298
   */
299
  public void newTextEditor() {
300
    open( DOCUMENT_DEFAULT );
301
  }
302
303
  /**
304
   * Opens a new definition editor document using the default definition
305
   * file name.
306
   */
307
  public void newDefinitionEditor() {
308
    open( DEFINITION_DEFAULT );
309
  }
310
311
  /**
312
   * Iterates over all tab panes to find all {@link TextEditor}s and request
313
   * that they save themselves.
314
   */
315
  public void saveAll() {
316
    mTabPanes.forEach(
317
      ( mt, tp ) -> tp.getTabs().forEach( ( tab ) -> {
318
        final var node = tab.getContent();
319
        if( node instanceof TextEditor ) {
320
          save( ((TextEditor) node) );
321
        }
322
      } )
323
    );
324
  }
325
326
  /**
327
   * Requests that the active {@link TextEditor} saves itself. Don't bother
328
   * checking if modified first because if the user swaps external media from
329
   * an external source (e.g., USB thumb drive), save should not second-guess
330
   * the user: save always re-saves. Also, it's less code.
331
   */
332
  public void save() {
333
    save( getActiveTextEditor() );
334
  }
335
336
  /**
337
   * Saves the active {@link TextEditor} under a new name.
338
   *
339
   * @param file The new active editor {@link File} reference.
340
   */
341
  public void saveAs( final File file ) {
342
    assert file != null;
343
    final var editor = getActiveTextEditor();
344
    final var tab = getTab( editor );
345
346
    editor.rename( file );
347
    tab.ifPresent( t -> {
348
      t.setText( editor.getFilename() );
349
      t.setTooltip( createTooltip( file ) );
350
    } );
351
352
    save();
353
  }
354
355
  /**
356
   * Saves the given {@link TextResource} to a file. This is typically used
357
   * to save either an instance of {@link TextEditor} or {@link TextDefinition}.
358
   *
359
   * @param resource The resource to export.
360
   */
361
  private void save( final TextResource resource ) {
362
    try {
363
      resource.save();
364
    } catch( final Exception ex ) {
365
      clue( ex );
366
      sNotifier.alert(
367
        getWindow(), resource.getPath(), "TextResource.saveFailed", ex
368
      );
369
    }
370
  }
371
372
  /**
373
   * Closes all open {@link TextEditor}s; all {@link TextDefinition}s stay open.
374
   *
375
   * @return {@code true} when all editors, modified or otherwise, were
376
   * permitted to close; {@code false} when one or more editors were modified
377
   * and the user requested no closing.
378
   */
379
  public boolean closeAll() {
380
    var closable = true;
381
382
    for( final var entry : mTabPanes.entrySet() ) {
383
      final var tabPane = entry.getValue();
384
      final var tabIterator = tabPane.getTabs().iterator();
385
386
      while( tabIterator.hasNext() ) {
387
        final var tab = tabIterator.next();
388
        final var resource = tab.getContent();
389
390
        // The definition panes auto-save, so being specific here prevents
391
        // closing the definitions in the situation where the user wants to
392
        // continue editing (i.e., possibly save unsaved work).
393
        if( !(resource instanceof TextEditor) ) {
394
          continue;
395
        }
396
397
        if( canClose( (TextEditor) resource ) ) {
398
          tabIterator.remove();
399
          close( tab );
400
        }
401
        else {
402
          closable = false;
403
        }
404
      }
405
    }
406
407
    return closable;
408
  }
409
410
  /**
411
   * Calls the tab's {@link Tab#getOnClosed()} handler to carry out a close
412
   * event.
413
   *
414
   * @param tab The {@link Tab} that was closed.
415
   */
416
  private void close( final Tab tab ) {
417
    final var handler = tab.getOnClosed();
418
419
    if( handler != null ) {
420
      handler.handle( new ActionEvent() );
421
    }
422
  }
423
424
  /**
425
   * Closes the active tab; delegates to {@link #canClose(TextResource)}.
426
   */
427
  public void close() {
428
    final var editor = getActiveTextEditor();
429
    if( canClose( editor ) ) {
430
      close( editor );
431
    }
432
  }
433
434
  /**
435
   * Closes the given {@link TextResource}. This must not be called from within
436
   * a loop that iterates over the tab panes using {@code forEach}, lest a
437
   * concurrent modification exception be thrown.
438
   *
439
   * @param resource The {@link TextResource} to close, without confirming with
440
   *                 the user.
441
   */
442
  private void close( final TextResource resource ) {
443
    getTab( resource ).ifPresent(
444
      ( tab ) -> {
445
        tab.getTabPane().getTabs().remove( tab );
446
        close( tab );
447
      }
448
    );
449
  }
450
451
  /**
452
   * Answers whether the given {@link TextResource} may be closed.
453
   *
454
   * @param editor The {@link TextResource} to try closing.
455
   * @return {@code true} when the editor may be closed; {@code false} when
456
   * the user has requested to keep the editor open.
457
   */
458
  private boolean canClose( final TextResource editor ) {
459
    final var editorTab = getTab( editor );
460
    final var canClose = new AtomicBoolean( true );
461
462
    if( editor.isModified() ) {
463
      final var filename = new StringBuilder();
464
      editorTab.ifPresent( ( tab ) -> filename.append( tab.getText() ) );
465
466
      final var message = sNotifier.createNotification(
467
        Messages.get( "Alert.file.close.title" ),
468
        Messages.get( "Alert.file.close.text" ),
469
        filename.toString()
470
      );
471
472
      final var dialog = sNotifier.createConfirmation( getWindow(), message );
473
474
      dialog.showAndWait().ifPresent(
475
        save -> canClose.set( save == YES ? editor.save() : save == NO )
476
      );
477
    }
478
479
    return canClose.get();
480
  }
481
482
  private ObjectProperty<TextEditor> createActiveTextEditor() {
483
    final var editor = new SimpleObjectProperty<TextEditor>();
484
485
    editor.addListener( ( c, o, n ) -> {
486
      if( n != null ) {
487
        mHtmlPreview.setBaseUri( n.getPath() );
488
        process( n );
489
      }
490
    } );
491
492
    return editor;
493
  }
494
495
  /**
496
   * Adds the HTML preview tab to its own, singular tab pane.
497
   */
498
  public void viewPreview() {
499
    viewTab( mHtmlPreview, TEXT_HTML, "HTML" );
500
  }
501
502
  /**
503
   * Adds the document outline tab to its own, singular tab pane.
504
   */
505
  public void viewOutline() {
506
    viewTab( mDocumentOutline, APP_DOCUMENT_OUTLINE, "Outline" );
507
  }
508
509
  private void viewTab(
510
    final Node node, final MediaType mediaType, final String name ) {
511
    final var tabPane = obtainTabPane( mediaType );
512
513
    for( final var tab : tabPane.getTabs() ) {
514
      if( tab.getContent() == node ) {
515
        return;
516
      }
517
    }
518
519
    tabPane.getTabs().add( createTab( name, node ) );
520
    addTabPane( tabPane );
521
  }
522
523
  public void viewRefresh() {
524
    mHtmlPreview.refresh();
525
  }
526
527
  /**
528
   * Returns the tab that contains the given {@link TextEditor}.
529
   *
530
   * @param editor The {@link TextEditor} instance to find amongst the tabs.
531
   * @return The first tab having content that matches the given tab.
532
   */
533
  private Optional<Tab> getTab( final TextResource editor ) {
534
    return mTabPanes.values()
535
                    .stream()
536
                    .flatMap( pane -> pane.getTabs().stream() )
537
                    .filter( tab -> editor.equals( tab.getContent() ) )
538
                    .findFirst();
539
  }
540
541
  /**
542
   * Creates a new {@link DefinitionEditor} wrapped in a listener that
543
   * is used to detect when the active {@link DefinitionEditor} has changed.
544
   * Upon changing, the {@link #mResolvedMap} is updated and the active
545
   * text editor is refreshed.
546
   *
547
   * @param editor Text editor to update with the revised resolved map.
548
   * @return A newly configured property that represents the active
549
   * {@link DefinitionEditor}, never null.
550
   */
551
  private ObjectProperty<TextDefinition> createActiveDefinitionEditor(
552
    final ObjectProperty<TextEditor> editor ) {
553
    final var definitions = new SimpleObjectProperty<TextDefinition>();
554
    definitions.addListener( ( c, o, n ) -> {
555
      resolve( n == null ? createDefinitionEditor() : n );
556
      process( editor.get() );
557
    } );
558
559
    return definitions;
560
  }
561
562
  private Tab createTab( final String filename, final Node node ) {
563
    return new DetachableTab( filename, node );
564
  }
565
566
  private Tab createTab( final File file ) {
567
    final var r = createTextResource( file );
568
    final var tab = createTab( r.getFilename(), r.getNode() );
569
570
    r.modifiedProperty().addListener(
571
      ( c, o, n ) -> tab.setText( r.getFilename() + (n ? "*" : "") )
572
    );
573
574
    // This is called when either the tab is closed by the user clicking on
575
    // the tab's close icon or when closing (all) from the file menu.
576
    tab.setOnClosed(
577
      ( __ ) -> getRecentFiles().remove( file.getAbsolutePath() )
578
    );
579
580
    tab.tabPaneProperty().addListener( ( cPane, oPane, nPane ) -> {
581
      if( nPane != null ) {
582
        nPane.focusedProperty().addListener( ( c, o, n ) -> {
583
          if( n != null && n ) {
584
            final var selected = nPane.getSelectionModel().getSelectedItem();
585
            final var node = selected.getContent();
586
            node.requestFocus();
587
          }
588
        } );
589
      }
590
    } );
591
592
    return tab;
593
  }
594
595
  /**
596
   * Creates bins for the different {@link MediaType}s, which eventually are
597
   * added to the UI as separate tab panes. If ever a general-purpose scene
598
   * exporter is developed to serialize a scene to an FXML file, this could
599
   * be replaced by such a class.
600
   * <p>
601
   * When binning the files, this makes sure that at least one file exists
602
   * for every type. If the user has opted to close a particular type (such
603
   * as the definition pane), the view will suppressed elsewhere.
604
   * </p>
605
   * <p>
606
   * The order that the binned files are returned will be reflected in the
607
   * order that the corresponding panes are rendered in the UI.
608
   * </p>
609
   *
610
   * @param paths The file paths to bin according to their type.
611
   * @return An in-order list of files, first by structured definition files,
612
   * then by plain text documents.
613
   */
614
  private List<File> bin( final SetProperty<String> paths ) {
615
    // Treat all files destined for the text editor as plain text documents
616
    // so that they are added to the same pane. Grouping by TEXT_PLAIN is a
617
    // bit arbitrary, but means explicitly capturing TEXT_PLAIN isn't needed.
618
    final Function<MediaType, MediaType> bin =
619
      m -> PLAIN_TEXT_FORMAT.contains( m ) ? TEXT_PLAIN : m;
620
621
    // Create two groups: YAML files and plain text files.
622
    final var bins = paths
623
      .stream()
624
      .collect(
625
        groupingBy( path -> bin.apply( MediaType.valueFrom( path ) ) )
626
      );
627
628
    bins.putIfAbsent( TEXT_YAML, List.of( DEFINITION_DEFAULT.toString() ) );
629
    bins.putIfAbsent( TEXT_PLAIN, List.of( DOCUMENT_DEFAULT.toString() ) );
630
631
    final var result = new ArrayList<File>( paths.size() );
632
633
    // Ensure that the same types are listed together (keep insertion order).
634
    bins.forEach( ( mediaType, files ) -> result.addAll(
635
      files.stream().map( File::new ).collect( Collectors.toList() ) )
636
    );
637
638
    return result;
639
  }
640
641
  /**
642
   * Uses the given {@link TextDefinition} instance to update the
643
   * {@link #mResolvedMap}.
644
   *
645
   * @param editor A non-null, possibly empty definition editor.
646
   */
647
  private void resolve( final TextDefinition editor ) {
648
    assert editor != null;
649
650
    final var tokens = createDefinitionTokens();
651
    final var operator = new YamlSigilOperator( tokens );
652
    final var map = new HashMap<String, String>();
653
654
    editor.toMap().forEach( ( k, v ) -> map.put( operator.entoken( k ), v ) );
655
656
    mResolvedMap.clear();
657
    mResolvedMap.putAll( editor.interpolate( map, tokens ) );
658
  }
659
660
  /**
661
   * Force the active editor to update, which will cause the processor
662
   * to re-evaluate the interpolated definition map thereby updating the
663
   * preview pane.
664
   *
665
   * @param editor Contains the source document to update in the preview pane.
666
   */
667
  private void process( final TextEditor editor ) {
668
    // Ensure that these are run from within the Swing event dispatch thread
669
    // so that the text editor thread is immediately freed for caret movement.
670
    // This means that the preview will have a slight delay when catching up
671
    // to the caret position.
672
    invokeLater( () -> {
673
      final var processor = mProcessors.getOrDefault( editor, IDENTITY );
674
      processor.apply( editor == null ? "" : editor.getText() );
675
      mHtmlPreview.scrollTo( CARET_ID );
676
    } );
677
  }
678
679
  /**
680
   * Lazily creates a {@link TabPane} configured to listen for tab select
681
   * events. The tab pane is associated with a given media type so that
682
   * similar files can be grouped together.
683
   *
684
   * @param mediaType The media type to associate with the tab pane.
685
   * @return An instance of {@link TabPane} that will handle tab docking.
686
   */
687
  private TabPane obtainTabPane( final MediaType mediaType ) {
688
    return mTabPanes.computeIfAbsent(
689
      mediaType, ( mt ) -> createTabPane()
690
    );
691
  }
692
693
  /**
694
   * Creates an initialized {@link TabPane} instance.
695
   *
696
   * @return A new {@link TabPane} with all listeners configured.
697
   */
698
  private TabPane createTabPane() {
699
    final var tabPane = new DetachableTabPane();
700
701
    initStageOwnerFactory( tabPane );
702
    initTabListener( tabPane );
703
704
    return tabPane;
705
  }
706
707
  /**
708
   * When any {@link DetachableTabPane} is detached from the main window,
709
   * the stage owner factory must be given its parent window, which will
710
   * own the child window. The parent window is the {@link MainPane}'s
711
   * {@link Scene}'s {@link Window} instance.
712
   *
713
   * <p>
714
   * This will derives the new title from the main window title, incrementing
715
   * the window count to help uniquely identify the child windows.
716
   * </p>
717
   *
718
   * @param tabPane A new {@link DetachableTabPane} to configure.
719
   */
720
  private void initStageOwnerFactory( final DetachableTabPane tabPane ) {
721
    tabPane.setStageOwnerFactory( ( stage ) -> {
722
      final var title = get(
723
        "Detach.tab.title",
724
        ((Stage) getWindow()).getTitle(), ++mWindowCount
725
      );
726
      stage.setTitle( title );
727
728
      return getScene().getWindow();
729
    } );
730
  }
731
732
  /**
733
   * Responsible for configuring the content of each {@link DetachableTab} when
734
   * it is added to the given {@link DetachableTabPane} instance.
735
   * <p>
736
   * For {@link TextEditor} contents, an instance of {@link ScrollEventHandler}
737
   * is initialized to perform synchronized scrolling between the editor and
738
   * its preview window. Additionally, the last tab in the tab pane's list of
739
   * tabs is given focus.
740
   * </p>
741
   * <p>
742
   * Note that multiple tabs can be added simultaneously.
743
   * </p>
744
   *
745
   * @param tabPane A new {@link TabPane} to configure.
746
   */
747
  private void initTabListener( final TabPane tabPane ) {
748
    tabPane.getTabs().addListener(
749
      ( final ListChangeListener.Change<? extends Tab> listener ) -> {
750
        while( listener.next() ) {
751
          if( listener.wasAdded() ) {
752
            final var tabs = listener.getAddedSubList();
753
754
            tabs.forEach( ( tab ) -> {
755
              final var node = tab.getContent();
756
757
              if( node instanceof TextEditor ) {
758
                initScrollEventListener( tab );
759
              }
760
            } );
761
762
            // Select and give focus to the last tab opened.
763
            final var index = tabs.size() - 1;
764
            if( index >= 0 ) {
765
              final var tab = tabs.get( index );
766
              tabPane.getSelectionModel().select( tab );
767
              tab.getContent().requestFocus();
768
            }
769
          }
770
        }
771
      }
772
    );
773
  }
774
775
  /**
776
   * Synchronizes scrollbar positions between the given {@link Tab} that
777
   * contains an instance of {@link TextEditor} and {@link HtmlPreview} pane.
778
   *
779
   * @param tab The container for an instance of {@link TextEditor}.
780
   */
781
  private void initScrollEventListener( final Tab tab ) {
782
    final var editor = (TextEditor) tab.getContent();
783
    final var scrollPane = editor.getScrollPane();
784
    final var scrollBar = mHtmlPreview.getVerticalScrollBar();
785
    final var handler = new ScrollEventHandler( scrollPane, scrollBar );
786
    handler.enabledProperty().bind( tab.selectedProperty() );
787
  }
788
789
  private void addTabPane( final int index, final TabPane tabPane ) {
790
    final var items = getItems();
791
    if( !items.contains( tabPane ) ) {
792
      items.add( index, tabPane );
793
    }
794
  }
795
796
  private void addTabPane( final TabPane tabPane ) {
797
    addTabPane( getItems().size(), tabPane );
798
  }
799
800
  public ProcessorContext createProcessorContext() {
801
    return createProcessorContext( NONE );
802
  }
803
804
  public ProcessorContext createProcessorContext( final ExportFormat format ) {
805
    final var editor = getActiveTextEditor();
806
    return createProcessorContext(
807
      editor.getPath(), editor.getCaret(), format );
808
  }
809
810
  /**
811
   * @param path  Used by {@link ProcessorFactory} to determine
812
   *              {@link Processor} type to create based on file type.
813
   * @param caret Used by {@link CaretExtension} to add ID attribute into
814
   *              preview document for scrollbar synchronization.
815
   * @return A new {@link ProcessorContext} to use when creating an instance of
816
   * {@link Processor}.
817
   */
818
  private ProcessorContext createProcessorContext(
819
    final Path path, final Caret caret, final ExportFormat format ) {
820
    return new ProcessorContext(
821
      mHtmlPreview, mResolvedMap, path, caret, format, mWorkspace
116
  private final HtmlPreview mPreview;
117
118
  /**
119
   * Provides an interactive document outline.
120
   */
121
  private final DocumentOutline mOutline = new DocumentOutline();
122
123
  /**
124
   * Changing the active editor fires the value changed event. This allows
125
   * refreshes to happen when external definitions are modified and need to
126
   * trigger the processing chain.
127
   */
128
  private final ObjectProperty<TextEditor> mActiveTextEditor =
129
    createActiveTextEditor();
130
131
  /**
132
   * Changing the active definition editor fires the value changed event. This
133
   * allows refreshes to happen when external definitions are modified and need
134
   * to trigger the processing chain.
135
   */
136
  private final ObjectProperty<TextDefinition> mActiveDefinitionEditor =
137
    createActiveDefinitionEditor( mActiveTextEditor );
138
139
  /**
140
   * Tracks the number of detached tab panels opened into their own windows,
141
   * which allows unique identification of subordinate windows by their title.
142
   * It is doubtful more than 128 windows, much less 256, will be created.
143
   */
144
  private byte mWindowCount;
145
146
  /**
147
   * Called when the definition data is changed.
148
   */
149
  private final EventHandler<TreeModificationEvent<Event>> mTreeHandler =
150
    event -> {
151
      final var editor = mActiveDefinitionEditor.get();
152
153
      resolve( editor );
154
      process( getActiveTextEditor() );
155
      save( editor );
156
    };
157
158
  /**
159
   * Adds all content panels to the main user interface. This will load the
160
   * configuration settings from the workspace to reproduce the settings from
161
   * a previous session.
162
   */
163
  public MainPane( final Workspace workspace ) {
164
    mWorkspace = workspace;
165
    mPreview = new HtmlPreview( workspace );
166
167
    open( bin( getRecentFiles() ) );
168
    viewPreview();
169
    setDividerPositions( calculateDividerPositions() );
170
171
    // Once the main scene's window regains focus, update the active definition
172
    // editor to the currently selected tab.
173
    runLater(
174
      () -> getWindow().setOnCloseRequest( ( event ) -> {
175
        // Order matters here. We want to close all the tabs to ensure each
176
        // is saved, but after they are closed, the workspace should still
177
        // retain the list of files that were open. If this line came after
178
        // closing, then restarting the application would list no files.
179
        mWorkspace.save();
180
181
        if( closeAll() ) {
182
          Platform.exit();
183
          System.exit( 0 );
184
        }
185
        else {
186
          event.consume();
187
        }
188
      } )
189
    );
190
191
    register( this );
192
  }
193
194
  @Subscribe
195
  public void handle( final TextEditorFocusEvent event ) {
196
    mActiveTextEditor.set( event.get() );
197
  }
198
199
  @Subscribe
200
  public void handle( final TextDefinitionFocusEvent event ) {
201
    mActiveDefinitionEditor.set( event.get() );
202
  }
203
204
  /**
205
   * Typically called when a file name is clicked in the {@link HtmlPanel}.
206
   *
207
   * @param event The event to process, must contain a valid file reference.
208
   */
209
  @Subscribe
210
  public void handle( final FileOpenEvent event ) {
211
    final File eventFile;
212
    final var eventUri = event.getUri();
213
214
    if( eventUri.isAbsolute() ) {
215
      eventFile = new File( eventUri.getPath() );
216
    }
217
    else {
218
      final var activeFile = getActiveTextEditor().getFile();
219
      final var parent = activeFile.getParentFile();
220
221
      if( parent == null ) {
222
        clue( new FileNotFoundException( eventUri.getPath() ) );
223
        return;
224
      }
225
      else {
226
        final var parentPath = parent.getAbsolutePath();
227
        eventFile = Path.of( parentPath, eventUri.getPath() ).toFile();
228
      }
229
    }
230
231
    runLater( () -> open( eventFile ) );
232
  }
233
234
  @Subscribe
235
  public void handle( final CaretNavigationEvent event ) {
236
    runLater( () -> {
237
      final var textArea = getActiveTextEditor().getTextArea();
238
      textArea.moveTo( event.getOffset() );
239
      textArea.requestFollowCaret();
240
      textArea.requestFocus();
241
    } );
242
  }
243
244
  /**
245
   * TODO: Load divider positions from exported settings, see bin() comment.
246
   */
247
  private double[] calculateDividerPositions() {
248
    final var ratio = 100f / getItems().size() / 100;
249
    final var positions = getDividerPositions();
250
251
    for( int i = 0; i < positions.length; i++ ) {
252
      positions[ i ] = ratio * i;
253
    }
254
255
    return positions;
256
  }
257
258
  /**
259
   * Opens all the files into the application, provided the paths are unique.
260
   * This may only be called for any type of files that a user can edit
261
   * (i.e., update and persist), such as definitions and text files.
262
   *
263
   * @param files The list of files to open.
264
   */
265
  public void open( final List<File> files ) {
266
    files.forEach( this::open );
267
  }
268
269
  /**
270
   * This opens the given file. Since the preview pane is not a file that
271
   * can be opened, it is safe to add a listener to the detachable pane.
272
   *
273
   * @param file The file to open.
274
   */
275
  private void open( final File file ) {
276
    final var tab = createTab( file );
277
    final var node = tab.getContent();
278
    final var mediaType = MediaType.valueFrom( file );
279
    final var tabPane = obtainTabPane( mediaType );
280
281
    tab.setTooltip( createTooltip( file ) );
282
    tabPane.setFocusTraversable( false );
283
    tabPane.setTabClosingPolicy( ALL_TABS );
284
    tabPane.getTabs().add( tab );
285
286
    // Attach the tab scene factory for new tab panes.
287
    if( !getItems().contains( tabPane ) ) {
288
      addTabPane(
289
        node instanceof TextDefinition ? 0 : getItems().size(), tabPane
290
      );
291
    }
292
293
    getRecentFiles().add( file.getAbsolutePath() );
294
  }
295
296
  /**
297
   * Opens a new text editor document using the default document file name.
298
   */
299
  public void newTextEditor() {
300
    open( DOCUMENT_DEFAULT );
301
  }
302
303
  /**
304
   * Opens a new definition editor document using the default definition
305
   * file name.
306
   */
307
  public void newDefinitionEditor() {
308
    open( DEFINITION_DEFAULT );
309
  }
310
311
  /**
312
   * Iterates over all tab panes to find all {@link TextEditor}s and request
313
   * that they save themselves.
314
   */
315
  public void saveAll() {
316
    mTabPanes.forEach(
317
      ( mt, tp ) -> tp.getTabs().forEach( ( tab ) -> {
318
        final var node = tab.getContent();
319
        if( node instanceof TextEditor ) {
320
          save( ((TextEditor) node) );
321
        }
322
      } )
323
    );
324
  }
325
326
  /**
327
   * Requests that the active {@link TextEditor} saves itself. Don't bother
328
   * checking if modified first because if the user swaps external media from
329
   * an external source (e.g., USB thumb drive), save should not second-guess
330
   * the user: save always re-saves. Also, it's less code.
331
   */
332
  public void save() {
333
    save( getActiveTextEditor() );
334
  }
335
336
  /**
337
   * Saves the active {@link TextEditor} under a new name.
338
   *
339
   * @param file The new active editor {@link File} reference.
340
   */
341
  public void saveAs( final File file ) {
342
    assert file != null;
343
    final var editor = getActiveTextEditor();
344
    final var tab = getTab( editor );
345
346
    editor.rename( file );
347
    tab.ifPresent( t -> {
348
      t.setText( editor.getFilename() );
349
      t.setTooltip( createTooltip( file ) );
350
    } );
351
352
    save();
353
  }
354
355
  /**
356
   * Saves the given {@link TextResource} to a file. This is typically used
357
   * to save either an instance of {@link TextEditor} or {@link TextDefinition}.
358
   *
359
   * @param resource The resource to export.
360
   */
361
  private void save( final TextResource resource ) {
362
    try {
363
      resource.save();
364
    } catch( final Exception ex ) {
365
      clue( ex );
366
      sNotifier.alert(
367
        getWindow(), resource.getPath(), "TextResource.saveFailed", ex
368
      );
369
    }
370
  }
371
372
  /**
373
   * Closes all open {@link TextEditor}s; all {@link TextDefinition}s stay open.
374
   *
375
   * @return {@code true} when all editors, modified or otherwise, were
376
   * permitted to close; {@code false} when one or more editors were modified
377
   * and the user requested no closing.
378
   */
379
  public boolean closeAll() {
380
    var closable = true;
381
382
    for( final var entry : mTabPanes.entrySet() ) {
383
      final var tabPane = entry.getValue();
384
      final var tabIterator = tabPane.getTabs().iterator();
385
386
      while( tabIterator.hasNext() ) {
387
        final var tab = tabIterator.next();
388
        final var resource = tab.getContent();
389
390
        // The definition panes auto-save, so being specific here prevents
391
        // closing the definitions in the situation where the user wants to
392
        // continue editing (i.e., possibly save unsaved work).
393
        if( !(resource instanceof TextEditor) ) {
394
          continue;
395
        }
396
397
        if( canClose( (TextEditor) resource ) ) {
398
          tabIterator.remove();
399
          close( tab );
400
        }
401
        else {
402
          closable = false;
403
        }
404
      }
405
    }
406
407
    return closable;
408
  }
409
410
  /**
411
   * Calls the tab's {@link Tab#getOnClosed()} handler to carry out a close
412
   * event.
413
   *
414
   * @param tab The {@link Tab} that was closed.
415
   */
416
  private void close( final Tab tab ) {
417
    final var handler = tab.getOnClosed();
418
419
    if( handler != null ) {
420
      handler.handle( new ActionEvent() );
421
    }
422
  }
423
424
  /**
425
   * Closes the active tab; delegates to {@link #canClose(TextResource)}.
426
   */
427
  public void close() {
428
    final var editor = getActiveTextEditor();
429
    if( canClose( editor ) ) {
430
      close( editor );
431
    }
432
  }
433
434
  /**
435
   * Closes the given {@link TextResource}. This must not be called from within
436
   * a loop that iterates over the tab panes using {@code forEach}, lest a
437
   * concurrent modification exception be thrown.
438
   *
439
   * @param resource The {@link TextResource} to close, without confirming with
440
   *                 the user.
441
   */
442
  private void close( final TextResource resource ) {
443
    getTab( resource ).ifPresent(
444
      ( tab ) -> {
445
        tab.getTabPane().getTabs().remove( tab );
446
        close( tab );
447
      }
448
    );
449
  }
450
451
  /**
452
   * Answers whether the given {@link TextResource} may be closed.
453
   *
454
   * @param editor The {@link TextResource} to try closing.
455
   * @return {@code true} when the editor may be closed; {@code false} when
456
   * the user has requested to keep the editor open.
457
   */
458
  private boolean canClose( final TextResource editor ) {
459
    final var editorTab = getTab( editor );
460
    final var canClose = new AtomicBoolean( true );
461
462
    if( editor.isModified() ) {
463
      final var filename = new StringBuilder();
464
      editorTab.ifPresent( ( tab ) -> filename.append( tab.getText() ) );
465
466
      final var message = sNotifier.createNotification(
467
        Messages.get( "Alert.file.close.title" ),
468
        Messages.get( "Alert.file.close.text" ),
469
        filename.toString()
470
      );
471
472
      final var dialog = sNotifier.createConfirmation( getWindow(), message );
473
474
      dialog.showAndWait().ifPresent(
475
        save -> canClose.set( save == YES ? editor.save() : save == NO )
476
      );
477
    }
478
479
    return canClose.get();
480
  }
481
482
  private ObjectProperty<TextEditor> createActiveTextEditor() {
483
    final var editor = new SimpleObjectProperty<TextEditor>();
484
485
    editor.addListener( ( c, o, n ) -> {
486
      if( n != null ) {
487
        mPreview.setBaseUri( n.getPath() );
488
        process( n );
489
      }
490
    } );
491
492
    return editor;
493
  }
494
495
  /**
496
   * Adds the HTML preview tab to its own, singular tab pane.
497
   */
498
  public void viewPreview() {
499
    viewTab( mPreview, TEXT_HTML, get( "Pane.preview.title" ) );
500
  }
501
502
  /**
503
   * Adds the document outline tab to its own, singular tab pane.
504
   */
505
  public void viewOutline() {
506
    viewTab( mOutline, APP_DOCUMENT_OUTLINE, get( "Pane.outline.title" ) );
507
  }
508
509
  private void viewTab(
510
    final Node node, final MediaType mediaType, final String name ) {
511
    final var tabPane = obtainTabPane( mediaType );
512
513
    for( final var tab : tabPane.getTabs() ) {
514
      if( tab.getContent() == node ) {
515
        return;
516
      }
517
    }
518
519
    tabPane.getTabs().add( createTab( name, node ) );
520
    addTabPane( tabPane );
521
  }
522
523
  public void viewRefresh() {
524
    mPreview.refresh();
525
  }
526
527
  /**
528
   * Returns the tab that contains the given {@link TextEditor}.
529
   *
530
   * @param editor The {@link TextEditor} instance to find amongst the tabs.
531
   * @return The first tab having content that matches the given tab.
532
   */
533
  private Optional<Tab> getTab( final TextResource editor ) {
534
    return mTabPanes.values()
535
                    .stream()
536
                    .flatMap( pane -> pane.getTabs().stream() )
537
                    .filter( tab -> editor.equals( tab.getContent() ) )
538
                    .findFirst();
539
  }
540
541
  /**
542
   * Creates a new {@link DefinitionEditor} wrapped in a listener that
543
   * is used to detect when the active {@link DefinitionEditor} has changed.
544
   * Upon changing, the {@link #mResolvedMap} is updated and the active
545
   * text editor is refreshed.
546
   *
547
   * @param editor Text editor to update with the revised resolved map.
548
   * @return A newly configured property that represents the active
549
   * {@link DefinitionEditor}, never null.
550
   */
551
  private ObjectProperty<TextDefinition> createActiveDefinitionEditor(
552
    final ObjectProperty<TextEditor> editor ) {
553
    final var definitions = new SimpleObjectProperty<TextDefinition>();
554
    definitions.addListener( ( c, o, n ) -> {
555
      resolve( n == null ? createDefinitionEditor() : n );
556
      process( editor.get() );
557
    } );
558
559
    return definitions;
560
  }
561
562
  private Tab createTab( final String filename, final Node node ) {
563
    return new DetachableTab( filename, node );
564
  }
565
566
  private Tab createTab( final File file ) {
567
    final var r = createTextResource( file );
568
    final var tab = createTab( r.getFilename(), r.getNode() );
569
570
    r.modifiedProperty().addListener(
571
      ( c, o, n ) -> tab.setText( r.getFilename() + (n ? "*" : "") )
572
    );
573
574
    // This is called when either the tab is closed by the user clicking on
575
    // the tab's close icon or when closing (all) from the file menu.
576
    tab.setOnClosed(
577
      ( __ ) -> getRecentFiles().remove( file.getAbsolutePath() )
578
    );
579
580
    tab.tabPaneProperty().addListener( ( cPane, oPane, nPane ) -> {
581
      if( nPane != null ) {
582
        nPane.focusedProperty().addListener( ( c, o, n ) -> {
583
          if( n != null && n ) {
584
            final var selected = nPane.getSelectionModel().getSelectedItem();
585
            final var node = selected.getContent();
586
            node.requestFocus();
587
          }
588
        } );
589
      }
590
    } );
591
592
    return tab;
593
  }
594
595
  /**
596
   * Creates bins for the different {@link MediaType}s, which eventually are
597
   * added to the UI as separate tab panes. If ever a general-purpose scene
598
   * exporter is developed to serialize a scene to an FXML file, this could
599
   * be replaced by such a class.
600
   * <p>
601
   * When binning the files, this makes sure that at least one file exists
602
   * for every type. If the user has opted to close a particular type (such
603
   * as the definition pane), the view will suppressed elsewhere.
604
   * </p>
605
   * <p>
606
   * The order that the binned files are returned will be reflected in the
607
   * order that the corresponding panes are rendered in the UI.
608
   * </p>
609
   *
610
   * @param paths The file paths to bin according to their type.
611
   * @return An in-order list of files, first by structured definition files,
612
   * then by plain text documents.
613
   */
614
  private List<File> bin( final SetProperty<String> paths ) {
615
    // Treat all files destined for the text editor as plain text documents
616
    // so that they are added to the same pane. Grouping by TEXT_PLAIN is a
617
    // bit arbitrary, but means explicitly capturing TEXT_PLAIN isn't needed.
618
    final Function<MediaType, MediaType> bin =
619
      m -> PLAIN_TEXT_FORMAT.contains( m ) ? TEXT_PLAIN : m;
620
621
    // Create two groups: YAML files and plain text files.
622
    final var bins = paths
623
      .stream()
624
      .collect(
625
        groupingBy( path -> bin.apply( MediaType.valueFrom( path ) ) )
626
      );
627
628
    bins.putIfAbsent( TEXT_YAML, List.of( DEFINITION_DEFAULT.toString() ) );
629
    bins.putIfAbsent( TEXT_PLAIN, List.of( DOCUMENT_DEFAULT.toString() ) );
630
631
    final var result = new ArrayList<File>( paths.size() );
632
633
    // Ensure that the same types are listed together (keep insertion order).
634
    bins.forEach( ( mediaType, files ) -> result.addAll(
635
      files.stream().map( File::new ).collect( Collectors.toList() ) )
636
    );
637
638
    return result;
639
  }
640
641
  /**
642
   * Uses the given {@link TextDefinition} instance to update the
643
   * {@link #mResolvedMap}.
644
   *
645
   * @param editor A non-null, possibly empty definition editor.
646
   */
647
  private void resolve( final TextDefinition editor ) {
648
    assert editor != null;
649
650
    final var tokens = createDefinitionTokens();
651
    final var operator = new YamlSigilOperator( tokens );
652
    final var map = new HashMap<String, String>();
653
654
    editor.toMap().forEach( ( k, v ) -> map.put( operator.entoken( k ), v ) );
655
656
    mResolvedMap.clear();
657
    mResolvedMap.putAll( editor.interpolate( map, tokens ) );
658
  }
659
660
  /**
661
   * Force the active editor to update, which will cause the processor
662
   * to re-evaluate the interpolated definition map thereby updating the
663
   * preview pane.
664
   *
665
   * @param editor Contains the source document to update in the preview pane.
666
   */
667
  private void process( final TextEditor editor ) {
668
    // Ensure that these are run from within the Swing event dispatch thread
669
    // so that the text editor thread is immediately freed for caret movement.
670
    // This means that the preview will have a slight delay when catching up
671
    // to the caret position.
672
    invokeLater( () -> {
673
      final var processor = mProcessors.getOrDefault( editor, IDENTITY );
674
      processor.apply( editor == null ? "" : editor.getText() );
675
      mPreview.scrollTo( CARET_ID );
676
    } );
677
  }
678
679
  /**
680
   * Lazily creates a {@link TabPane} configured to listen for tab select
681
   * events. The tab pane is associated with a given media type so that
682
   * similar files can be grouped together.
683
   *
684
   * @param mediaType The media type to associate with the tab pane.
685
   * @return An instance of {@link TabPane} that will handle tab docking.
686
   */
687
  private TabPane obtainTabPane( final MediaType mediaType ) {
688
    return mTabPanes.computeIfAbsent(
689
      mediaType, ( mt ) -> createTabPane()
690
    );
691
  }
692
693
  /**
694
   * Creates an initialized {@link TabPane} instance.
695
   *
696
   * @return A new {@link TabPane} with all listeners configured.
697
   */
698
  private TabPane createTabPane() {
699
    final var tabPane = new DetachableTabPane();
700
701
    initStageOwnerFactory( tabPane );
702
    initTabListener( tabPane );
703
704
    return tabPane;
705
  }
706
707
  /**
708
   * When any {@link DetachableTabPane} is detached from the main window,
709
   * the stage owner factory must be given its parent window, which will
710
   * own the child window. The parent window is the {@link MainPane}'s
711
   * {@link Scene}'s {@link Window} instance.
712
   *
713
   * <p>
714
   * This will derives the new title from the main window title, incrementing
715
   * the window count to help uniquely identify the child windows.
716
   * </p>
717
   *
718
   * @param tabPane A new {@link DetachableTabPane} to configure.
719
   */
720
  private void initStageOwnerFactory( final DetachableTabPane tabPane ) {
721
    tabPane.setStageOwnerFactory( ( stage ) -> {
722
      final var title = get(
723
        "Detach.tab.title",
724
        ((Stage) getWindow()).getTitle(), ++mWindowCount
725
      );
726
      stage.setTitle( title );
727
728
      return getScene().getWindow();
729
    } );
730
  }
731
732
  /**
733
   * Responsible for configuring the content of each {@link DetachableTab} when
734
   * it is added to the given {@link DetachableTabPane} instance.
735
   * <p>
736
   * For {@link TextEditor} contents, an instance of {@link ScrollEventHandler}
737
   * is initialized to perform synchronized scrolling between the editor and
738
   * its preview window. Additionally, the last tab in the tab pane's list of
739
   * tabs is given focus.
740
   * </p>
741
   * <p>
742
   * Note that multiple tabs can be added simultaneously.
743
   * </p>
744
   *
745
   * @param tabPane A new {@link TabPane} to configure.
746
   */
747
  private void initTabListener( final TabPane tabPane ) {
748
    tabPane.getTabs().addListener(
749
      ( final ListChangeListener.Change<? extends Tab> listener ) -> {
750
        while( listener.next() ) {
751
          if( listener.wasAdded() ) {
752
            final var tabs = listener.getAddedSubList();
753
754
            tabs.forEach( ( tab ) -> {
755
              final var node = tab.getContent();
756
757
              if( node instanceof TextEditor ) {
758
                initScrollEventListener( tab );
759
              }
760
            } );
761
762
            // Select and give focus to the last tab opened.
763
            final var index = tabs.size() - 1;
764
            if( index >= 0 ) {
765
              final var tab = tabs.get( index );
766
              tabPane.getSelectionModel().select( tab );
767
              tab.getContent().requestFocus();
768
            }
769
          }
770
        }
771
      }
772
    );
773
  }
774
775
  /**
776
   * Synchronizes scrollbar positions between the given {@link Tab} that
777
   * contains an instance of {@link TextEditor} and {@link HtmlPreview} pane.
778
   *
779
   * @param tab The container for an instance of {@link TextEditor}.
780
   */
781
  private void initScrollEventListener( final Tab tab ) {
782
    final var editor = (TextEditor) tab.getContent();
783
    final var scrollPane = editor.getScrollPane();
784
    final var scrollBar = mPreview.getVerticalScrollBar();
785
    final var handler = new ScrollEventHandler( scrollPane, scrollBar );
786
    handler.enabledProperty().bind( tab.selectedProperty() );
787
  }
788
789
  private void addTabPane( final int index, final TabPane tabPane ) {
790
    final var items = getItems();
791
    if( !items.contains( tabPane ) ) {
792
      items.add( index, tabPane );
793
    }
794
  }
795
796
  private void addTabPane( final TabPane tabPane ) {
797
    addTabPane( getItems().size(), tabPane );
798
  }
799
800
  public ProcessorContext createProcessorContext() {
801
    return createProcessorContext( NONE );
802
  }
803
804
  public ProcessorContext createProcessorContext( final ExportFormat format ) {
805
    final var editor = getActiveTextEditor();
806
    return createProcessorContext(
807
      editor.getPath(), editor.getCaret(), format );
808
  }
809
810
  /**
811
   * @param path  Used by {@link ProcessorFactory} to determine
812
   *              {@link Processor} type to create based on file type.
813
   * @param caret Used by {@link CaretExtension} to add ID attribute into
814
   *              preview document for scrollbar synchronization.
815
   * @return A new {@link ProcessorContext} to use when creating an instance of
816
   * {@link Processor}.
817
   */
818
  private ProcessorContext createProcessorContext(
819
    final Path path, final Caret caret, final ExportFormat format ) {
820
    return new ProcessorContext(
821
      mPreview, mResolvedMap, path, caret, format, mWorkspace
822822
    );
823823
  }
M src/main/java/com/keenwrite/editors/definition/DefinitionEditor.java
55
import com.keenwrite.editors.TextDefinition;
66
import com.keenwrite.sigils.Tokens;
7
import com.keenwrite.ui.tree.AltTreeView;
8
import com.keenwrite.ui.tree.TreeItemConverter;
79
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
810
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
...
5355
   */
5456
  private final DefinitionTreeItem<String> mTreeRoot = createRootTreeItem();
57
58
  /**
59
   * Converts a tree item value to and from a string..
60
   */
61
  private final TreeItemConverter mConverter = new TreeItemConverter();
5562
5663
  /**
5764
   * Contains a view of the definitions.
5865
   */
59
  private final TreeView<String> mTreeView = new TreeView<>( mTreeRoot );
66
  private final TreeView<String> mTreeView =
67
    new AltTreeView<>( mTreeRoot, mConverter );
6068
6169
  /**
...
112120
    mTreeTransformer = treeTransformer;
113121
114
    mTreeView.setEditable( true );
115
    mTreeView.setCellFactory( new TreeCellFactory() );
122
    //mTreeView.setCellFactory( new TreeCellFactory() );
116123
    mTreeView.setContextMenu( createContextMenu() );
117124
    mTreeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
118
    mTreeView.setShowRoot( false );
119125
    mTreeView.focusedProperty().addListener( this::focused );
120126
    getSelectionModel().setSelectionMode( MULTIPLE );
D src/main/java/com/keenwrite/editors/definition/FocusAwareTextFieldTreeCell.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import javafx.scene.Node;
5
import javafx.scene.control.TextField;
6
import javafx.scene.control.cell.TextFieldTreeCell;
7
import javafx.util.StringConverter;
8
9
/**
10
 * Responsible for fixing a focus lost bug in the JavaFX implementation.
11
 * See https://bugs.openjdk.java.net/browse/JDK-8089514 for details.
12
 * This implementation borrows from the official documentation on creating
13
 * tree views: https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm
14
 */
15
public class FocusAwareTextFieldTreeCell extends TextFieldTreeCell<String> {
16
  private TextField mTextField;
17
18
  public FocusAwareTextFieldTreeCell(
19
      final StringConverter<String> converter ) {
20
    super( converter );
21
  }
22
23
  @Override
24
  public void startEdit() {
25
    super.startEdit();
26
    var textField = mTextField;
27
28
    if( textField == null ) {
29
      textField = createTextField();
30
    }
31
    else {
32
      textField.setText( getItem() );
33
    }
34
35
    setText( null );
36
    setGraphic( textField );
37
    textField.selectAll();
38
    textField.requestFocus();
39
40
    // When the focus is lost, commit the edit then close the input field.
41
    // This fixes the unexpected behaviour when user clicks away.
42
    textField.focusedProperty().addListener( ( l, o, n ) -> {
43
      if( !n ) {
44
        commitEdit( mTextField.getText() );
45
      }
46
    } );
47
48
    mTextField = textField;
49
  }
50
51
  @Override
52
  public void cancelEdit() {
53
    super.cancelEdit();
54
    setText( getItem() );
55
    setGraphic( getTreeItem().getGraphic() );
56
  }
57
58
  @Override
59
  public void updateItem( String item, boolean empty ) {
60
    super.updateItem( item, empty );
61
62
    String text = null;
63
    Node graphic = null;
64
65
    if( !empty ) {
66
      if( isEditing() ) {
67
        final var textField = mTextField;
68
69
        if( textField != null ) {
70
          textField.setText( getString() );
71
        }
72
73
        graphic = textField;
74
      }
75
      else {
76
        text = getString();
77
        graphic = getTreeItem().getGraphic();
78
      }
79
    }
80
81
    setText( text );
82
    setGraphic( graphic );
83
  }
84
85
  private TextField createTextField() {
86
    final var textField = new TextField( getString() );
87
88
    textField.setOnKeyReleased( t -> {
89
      switch( t.getCode() ) {
90
        case ENTER -> commitEdit( textField.getText() );
91
        case ESCAPE -> cancelEdit();
92
      }
93
    } );
94
95
    return textField;
96
  }
97
98
  private String getString() {
99
    return getConverter().toString( getItem() );
100
  }
101
}
1021
D src/main/java/com/keenwrite/editors/definition/TreeCellFactory.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import javafx.collections.ObservableList;
5
import javafx.scene.control.TreeCell;
6
import javafx.scene.control.TreeItem;
7
import javafx.scene.control.TreeView;
8
import javafx.scene.input.ClipboardContent;
9
import javafx.scene.input.DataFormat;
10
import javafx.scene.input.DragEvent;
11
import javafx.scene.input.MouseEvent;
12
import javafx.util.Callback;
13
import javafx.util.StringConverter;
14
15
import java.util.Objects;
16
17
import static com.keenwrite.io.MediaType.APP_JAVA_OBJECT;
18
import static javafx.scene.input.TransferMode.MOVE;
19
20
/**
21
 * Responsible for producing {@link TreeCell} instances that can be edited
22
 * and respond to drag and drop functionality.
23
 */
24
public final class TreeCellFactory
25
    implements Callback<TreeView<String>, TreeCell<String>> {
26
  private static final String STYLE_CLASS_DROP_TARGET = "drop-target";
27
  private static final DataFormat JAVA_FORMAT =
28
      new DataFormat( APP_JAVA_OBJECT.toString() );
29
30
  private TreeItem<String> mDraggedTreeItem;
31
  private TreeCell<String> mTargetCell;
32
33
  /**
34
   * Constructs a new {@link TreeCell} manufacturing facility called when
35
   * a new {@link TreeItem} is added to one of the editor's {@link TreeView}s.
36
   */
37
  public TreeCellFactory() {
38
  }
39
40
  @Override
41
  public TreeCell<String> call( final TreeView<String> treeView ) {
42
    final var cell = createTreeCell();
43
44
    cell.setOnDragDetected( event -> dragDetected( event, cell ) );
45
    cell.setOnDragOver( event -> dragOver( event, cell ) );
46
    cell.setOnDragDropped( event -> dragDropped( event, cell, treeView ) );
47
    cell.setOnDragDone( event -> dragClear() );
48
49
    return cell;
50
  }
51
52
  private TreeCell<String> createTreeCell() {
53
    return new FocusAwareTextFieldTreeCell( createStringConverter() ) {
54
      @Override
55
      public void commitEdit( final String newValue ) {
56
        super.commitEdit( newValue );
57
        //mEditor.select( getTreeItem() );
58
        requestFocus();
59
      }
60
    };
61
  }
62
63
  private StringConverter<String> createStringConverter() {
64
    return new StringConverter<>() {
65
      @Override
66
      public String toString( final String object ) {
67
        return sanitize( object );
68
      }
69
70
      @Override
71
      public String fromString( final String string ) {
72
        return sanitize( string );
73
      }
74
75
      private String sanitize( final String string ) {
76
        return string == null ? "" : string;
77
      }
78
    };
79
  }
80
81
  /**
82
   * Drag start.
83
   *
84
   * @param event    The drag start {@link MouseEvent}.
85
   * @param treeCell The cell being dragged.
86
   */
87
  private void dragDetected(
88
      final MouseEvent event, final TreeCell<String> treeCell ) {
89
    final var sourceItem = treeCell.getTreeItem();
90
91
    // Prevent dragging the root item.
92
    if( sourceItem != null && sourceItem.getParent() != null ) {
93
      final var dragboard = treeCell.startDragAndDrop( MOVE );
94
      final var clipboard = new ClipboardContent();
95
      clipboard.put( JAVA_FORMAT, sourceItem.getValue() );
96
      dragboard.setContent( clipboard );
97
      dragboard.setDragView( treeCell.snapshot( null, null ) );
98
      event.consume();
99
100
      mDraggedTreeItem = sourceItem;
101
    }
102
  }
103
104
  /**
105
   * Drag over another {@link TreeCell} instance.
106
   *
107
   * @param event    The drag over {@link DragEvent}.
108
   * @param treeCell The cell dragged over.
109
   * @throws IllegalStateException Drag transfer "move" mode denied.
110
   */
111
  private void dragOver(
112
      final DragEvent event, final TreeCell<String> treeCell ) {
113
    if( event.getDragboard().hasContent( JAVA_FORMAT ) ) {
114
      final var thisItem = treeCell.getTreeItem();
115
116
      if( mDraggedTreeItem == null ||
117
          thisItem == null ||
118
          thisItem == mDraggedTreeItem ) {
119
        return;
120
      }
121
122
      // Ignore dragging over the root item.
123
      if( mDraggedTreeItem.getParent() == null ) {
124
        dragClear();
125
        return;
126
      }
127
128
      event.acceptTransferModes( MOVE );
129
130
      if( !Objects.equals( mTargetCell, treeCell ) ) {
131
        dragClear();
132
        mTargetCell = treeCell;
133
        mTargetCell.getStyleClass().add( STYLE_CLASS_DROP_TARGET );
134
      }
135
    }
136
  }
137
138
  /**
139
   * Dragged item is dropped
140
   *
141
   * @param event    The drag dropped {@link DragEvent}.
142
   * @param treeCell The cell dropped onto.
143
   */
144
  private void dragDropped( final DragEvent event,
145
                            final TreeCell<String> treeCell,
146
                            final TreeView<String> treeView ) {
147
    if( !event.getDragboard().hasContent( JAVA_FORMAT ) ) {
148
      return;
149
    }
150
151
    final var sourceItem = mDraggedTreeItem;
152
    final var sourceItemParent = mDraggedTreeItem.getParent();
153
    final var targetItem = treeCell.getTreeItem();
154
    final var targetItemParent = targetItem.getParent();
155
156
    sourceItemParent.getChildren().remove( sourceItem );
157
158
    final ObservableList<TreeItem<String>> children;
159
    final int index;
160
161
    // Dropping onto a parent node makes the source item the first child.
162
    if( Objects.equals( sourceItemParent, targetItem ) ) {
163
      children = targetItem.getChildren();
164
      index = 0;
165
    }
166
    else if( targetItemParent != null) {
167
      children = targetItemParent.getChildren();
168
      index = children.indexOf( targetItem ) + 1;
169
    }
170
    else {
171
      children = sourceItemParent.getChildren();
172
      index = 0;
173
    }
174
175
    children.add( index, sourceItem );
176
177
    treeView.getSelectionModel().clearSelection();
178
    treeView.getSelectionModel().select( sourceItem );
179
180
    // TODO: Notify a listener of the old and new tree item position.
181
182
    event.setDropCompleted( true );
183
  }
184
185
  private void dragClear() {
186
    final var targetCell = mTargetCell;
187
188
    if( targetCell != null ) {
189
      targetCell.getStyleClass().remove( STYLE_CLASS_DROP_TARGET );
190
    }
191
  }
192
}
1931
M src/main/java/com/keenwrite/io/HttpMediaType.java
22
package com.keenwrite.io;
33
4
import javax.net.ssl.*;
54
import java.net.MalformedURLException;
6
import java.net.Socket;
75
import java.net.URI;
86
import java.net.URL;
97
import java.net.http.HttpClient;
108
import java.net.http.HttpRequest;
11
import java.security.cert.X509Certificate;
129
1310
import static com.keenwrite.events.StatusEvent.clue;
...
2320
 */
2421
public final class HttpMediaType {
25
26
  static {
27
    disableSSLVerification();
28
  }
2922
3023
  private static final HttpClient HTTP_CLIENT = HttpClient
...
8477
8578
    return mediaType[ 0 ];
86
  }
87
88
  // Method used for bypassing SSL verification
89
  private static void disableSSLVerification() {
90
91
    TrustManager[] trustAllCerts =
92
      new TrustManager[]{new X509ExtendedTrustManager() {
93
        @Override
94
        public void checkClientTrusted( X509Certificate[] chain,
95
                                        String authType,
96
                                        Socket socket ) {
97
98
        }
99
100
        @Override
101
        public void checkServerTrusted( X509Certificate[] chain,
102
                                        String authType,
103
                                        Socket socket ) {
104
105
        }
106
107
        @Override
108
        public void checkClientTrusted( X509Certificate[] chain,
109
                                        String authType,
110
                                        SSLEngine engine ) {
111
112
        }
113
114
        @Override
115
        public void checkServerTrusted( X509Certificate[] chain,
116
                                        String authType,
117
                                        SSLEngine engine ) {
118
119
        }
120
121
        @Override
122
        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
123
          return null;
124
        }
125
126
        @Override
127
        public void checkClientTrusted(
128
          X509Certificate[] certs, String authType ) {
129
        }
130
131
        @Override
132
        public void checkServerTrusted(
133
          X509Certificate[] certs, String authType ) {
134
        }
135
      }};
136
137
    try {
138
      final var context = SSLContext.getInstance( "SSL" );
139
      context.init( null, trustAllCerts, new java.security.SecureRandom() );
140
      HttpsURLConnection.setDefaultSSLSocketFactory( context.getSocketFactory() );
141
      HttpsURLConnection.setDefaultHostnameVerifier( ( hostname, session ) -> true );
142
    } catch( final Exception ex ) {
143
      clue( ex );
144
    }
14579
  }
14680
}
M src/main/java/com/keenwrite/preview/HtmlPreview.java
22
package com.keenwrite.preview;
33
4
import com.keenwrite.Constants;
5
import com.keenwrite.preferences.LocaleProperty;
6
import com.keenwrite.preferences.Workspace;
7
import javafx.application.Platform;
8
import javafx.beans.property.DoubleProperty;
9
import javafx.beans.property.StringProperty;
10
import javafx.embed.swing.SwingNode;
11
import org.xhtmlrenderer.render.Box;
12
import org.xhtmlrenderer.swing.SwingReplacedElementFactory;
13
14
import javax.swing.*;
15
import java.awt.*;
16
import java.net.URL;
17
import java.nio.file.Path;
18
import java.util.Locale;
19
20
import static com.keenwrite.Constants.*;
21
import static com.keenwrite.Messages.get;
22
import static com.keenwrite.events.StatusEvent.clue;
23
import static com.keenwrite.preferences.WorkspaceKeys.*;
24
import static java.lang.Math.max;
25
import static java.lang.String.format;
26
import static java.lang.Thread.sleep;
27
import static javafx.application.Platform.runLater;
28
import static javafx.scene.CacheHint.SPEED;
29
import static javax.swing.SwingUtilities.invokeLater;
30
31
/**
32
 * Responsible for parsing an HTML document.
33
 */
34
public final class HtmlPreview extends SwingNode {
35
36
  /**
37
   * The order is important: Swing factory will replace SVG images with
38
   * a blank image, which will cause the chained factory to cache the image
39
   * and exit. Instead, the SVG must execute first to rasterize the content.
40
   * Consequently, the chained factory must maintain insertion order.
41
   */
42
  private static final ChainedReplacedElementFactory FACTORY
43
    = new ChainedReplacedElementFactory(
44
    new SvgReplacedElementFactory(),
45
    new SwingReplacedElementFactory()
46
  );
47
48
  /**
49
   * Used to populate the {@link #HTML_HEAD} with stylesheet file references.
50
   */
51
  private static final String HTML_STYLESHEET =
52
    "<link rel='stylesheet' href='%s'/>";
53
54
  /**
55
   * Render CSS using points (pt) not pixels (px) to reduce the chance of
56
   * poor rendering. The {@link #head()} method fills out the placeholders.
57
   * When the user has not set a locale, only one stylesheet is added to
58
   * the document.
59
   * <p>
60
   * Do not use points, only pixels here.
61
   * </p>
62
   */
63
  private static final String HTML_HEAD =
64
    """
65
      <!doctype html>
66
      <html lang='%s'><head><title> </title><meta charset='utf-8'/>
67
      %s%s<style>body{font-family:'%s';font-size: %dpx;}</style>
68
      <base href='%s'/></head><body>
69
      """;
70
71
  private static final String HTML_TAIL = "</body></html>";
72
73
  private static final URL HTML_STYLE_PREVIEW = toUrl( STYLESHEET_PREVIEW );
74
75
  /**
76
   * The buffer is reused so that previous memory allocations need not repeat.
77
   */
78
  private final StringBuilder mHtmlDocument = new StringBuilder( 65536 );
79
80
  private HtmlPanel mView;
81
  private JScrollPane mScrollPane;
82
  private String mBaseUriPath = "";
83
84
  /**
85
   * Populates {@link Constants#STYLESHEET_PREVIEW_LOCALE} for stylesheet.
86
   */
87
  private URL mLocaleUrl;
88
89
  private final Workspace mWorkspace;
90
91
  /**
92
   * Creates a new preview pane that can scroll to the caret position within the
93
   * document.
94
   *
95
   * @param workspace Contains locale and font size information.
96
   */
97
  public HtmlPreview( final Workspace workspace ) {
98
    mWorkspace = workspace;
99
    mLocaleUrl = toUrl( getLocale() );
100
101
    // Attempts to prevent a flash of black un-styled content upon load.
102
    setStyle( "-fx-background-color: white;" );
103
104
    invokeLater( () -> {
105
      mView = new HtmlPanel();
106
      mScrollPane = new JScrollPane( mView );
107
108
      // Enabling the cache attempts to prevent black flashes when resizing.
109
      setCache( true );
110
      setCacheHint( SPEED );
111
      setContent( mScrollPane );
112
113
      final var context = mView.getSharedContext();
114
      final var textRenderer = context.getTextRenderer();
115
      context.setReplacedElementFactory( FACTORY );
116
      textRenderer.setSmoothingThreshold( 0 );
117
118
      localeProperty().addListener( ( c, o, n ) -> {
119
        mLocaleUrl = toUrl( getLocale() );
120
        rerender();
121
      } );
122
123
      fontFamilyProperty().addListener( ( c, o, n ) -> rerender() );
124
      fontSizeProperty().addListener( ( c, o, n ) -> rerender() );
125
    } );
126
  }
127
128
  /**
129
   * Updates the internal HTML source shown in the preview pane.
130
   *
131
   * @param html The new HTML document to display.
132
   */
133
  public void render( final String html ) {
134
    mView.render( decorate( html ), getBaseUri() );
135
  }
136
137
  /**
138
   * Clears the caches then rerenders the content.
139
   */
140
  public void refresh() {
141
    FACTORY.clearCache();
142
    rerender();
143
  }
144
145
  private void rerender() {
146
    render( mHtmlDocument.toString() );
147
  }
148
149
  /**
150
   * Attaches the HTML head prefix and HTML tail suffix to the given HTML
151
   * string.
152
   *
153
   * @param html The HTML to adorn with opening and closing tags.
154
   * @return A complete HTML document, ready for rendering.
155
   */
156
  private String decorate( final String html ) {
157
    mHtmlDocument.setLength( 0 );
158
    mHtmlDocument.append( head() );
159
    mHtmlDocument.append( html );
160
    mHtmlDocument.append( tail() );
161
    return mHtmlDocument.toString();
162
  }
163
164
  private String head() {
165
    return format(
166
      HTML_HEAD,
167
      getLocale().getLanguage(),
168
      format( HTML_STYLESHEET, HTML_STYLE_PREVIEW ),
169
      mLocaleUrl == null ? "" : format( HTML_STYLESHEET, mLocaleUrl ),
170
      getFontFamily(),
171
      (int) (getFontSize() * (1 + 1 / 3f)),
172
      mBaseUriPath
173
    );
174
  }
175
176
  private String tail() {
177
    return HTML_TAIL;
178
  }
179
180
  /**
181
   * Clears the preview pane by rendering an empty string.
182
   */
183
  public void clear() {
184
    render( "" );
185
  }
186
187
  /**
188
   * Sets the base URI to the containing directory the file being edited.
189
   *
190
   * @param path The path to the file being edited.
191
   */
192
  public void setBaseUri( final Path path ) {
193
    final var parent = path.getParent();
194
    mBaseUriPath = parent == null ? "" : parent.toUri().toString();
195
  }
196
197
  /**
198
   * Scrolls to the closest element matching the given identifier without
199
   * waiting for the document to be ready.
200
   *
201
   * @param id Scroll the preview pane to this unique paragraph identifier.
202
   */
203
  public void scrollTo( final String id ) {
204
    final Runnable scrollToBox = () -> {
205
      int iter = 0;
206
      Box box = null;
207
208
      while( iter++ < 3 && ((box = mView.getBoxById( id )) == null) ) {
209
        try {
210
          sleep( 10 );
211
        } catch( final Exception ex ) {
212
          clue( ex );
213
        }
214
      }
215
216
      scrollTo( box );
217
    };
218
219
    if( Platform.isFxApplicationThread() ) {
220
      scrollToBox.run();
221
    }
222
    else {
223
      runLater( scrollToBox );
224
    }
225
  }
226
227
  /**
228
   * Scrolls to the location specified by the {@link Box} that corresponds
229
   * to a point somewhere in the preview pane. If there is no caret, then
230
   * this will not change the scroll position. Changing the scroll position
231
   * to the top if the {@link Box} instance is {@code null} will result in
232
   * jumping around a lot and inconsistent synchronization issues.
233
   *
234
   * @param box The rectangular region containing the caret, or {@code null}
235
   *            if the HTML does not have a caret.
236
   */
237
  private void scrollTo( final Box box ) {
238
    if( box != null ) {
239
      scrollTo( createPoint( box ) );
240
    }
241
  }
242
243
  private void scrollTo( final Point point ) {
244
    invokeLater( () -> {
245
      mView.scrollTo( point );
246
      getScrollPane().repaint();
247
    } );
248
  }
249
250
  /**
251
   * Creates a {@link Point} to use as a reference for scrolling to the area
252
   * described by the given {@link Box}. The {@link Box} coordinates are used
253
   * to populate the {@link Point}'s location, with minor adjustments for
254
   * vertical centering.
255
   *
256
   * @param box The {@link Box} that represents a scrolling anchor reference.
257
   * @return A coordinate suitable for scrolling to.
258
   */
259
  private Point createPoint( final Box box ) {
260
    assert box != null;
261
262
    // Scroll back up by half the height of the scroll bar to keep the typing
263
    // area within the view port. Otherwise the view port will have jumped too
264
    // high up and the most recently typed letters won't be visible.
265
    int y = max( box.getAbsY() - getVerticalScrollBarHeight() / 2, 0 );
266
    int x = box.getAbsX();
267
268
    if( !box.getStyle().isInline() ) {
269
      final var margin = box.getMargin( mView.getLayoutContext() );
270
      y += margin.top();
271
      x += margin.left();
272
    }
273
274
    return new Point( x, y );
275
  }
276
277
  private String getBaseUri() {
278
    return mBaseUriPath;
279
  }
280
281
  private JScrollPane getScrollPane() {
282
    return mScrollPane;
283
  }
284
285
  public JScrollBar getVerticalScrollBar() {
286
    return getScrollPane().getVerticalScrollBar();
287
  }
288
289
  private int getVerticalScrollBarHeight() {
290
    return getVerticalScrollBar().getHeight();
291
  }
292
293
  /**
294
   * Returns the ISO 639 alpha-2 or alpha-3 language code followed by a hyphen
295
   * followed by the ISO 15924 alpha-4 script code, followed by an ISO 3166
296
   * alpha-2 country code or UN M.49 numeric-3 area code. For example, this
297
   * could return "en-Latn-CA" for Canadian English written in the Latin
298
   * character set.
299
   *
300
   * @return Unique identifier for language and country.
301
   */
302
  private static URL toUrl( final Locale locale ) {
303
    return toUrl(
304
      get(
305
        sSettings.getSetting( STYLESHEET_PREVIEW_LOCALE, "" ),
306
        locale.getLanguage(),
307
        locale.getScript(),
308
        locale.getCountry()
309
      )
310
    );
311
  }
312
313
  private static URL toUrl( final String path ) {
314
    return HtmlPreview.class.getResource( path );
315
  }
316
317
  private Locale getLocale() {
318
    return localeProperty().toLocale();
319
  }
320
321
  private LocaleProperty localeProperty() {
322
    return mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
323
  }
324
325
  private String getFontFamily() {
326
    return fontFamilyProperty().get();
327
  }
328
329
  private StringProperty fontFamilyProperty() {
330
    return mWorkspace.stringProperty( KEY_UI_FONT_PREVIEW_NAME );
331
  }
332
333
  private double getFontSize() {
334
    return fontSizeProperty().get();
335
  }
336
337
  /**
338
   * Returns the font size in points.
4
import com.keenwrite.preferences.LocaleProperty;
5
import com.keenwrite.preferences.Workspace;
6
import javafx.application.Platform;
7
import javafx.beans.property.DoubleProperty;
8
import javafx.beans.property.StringProperty;
9
import javafx.embed.swing.SwingNode;
10
import org.xhtmlrenderer.render.Box;
11
import org.xhtmlrenderer.swing.SwingReplacedElementFactory;
12
13
import javax.swing.*;
14
import java.awt.*;
15
import java.net.URL;
16
import java.nio.file.Path;
17
import java.util.Locale;
18
19
import static com.keenwrite.Constants.*;
20
import static com.keenwrite.Messages.get;
21
import static com.keenwrite.events.StatusEvent.clue;
22
import static com.keenwrite.preferences.WorkspaceKeys.*;
23
import static java.lang.Math.max;
24
import static java.lang.String.format;
25
import static java.lang.Thread.sleep;
26
import static javafx.application.Platform.runLater;
27
import static javafx.scene.CacheHint.SPEED;
28
import static javax.swing.SwingUtilities.invokeLater;
29
30
/**
31
 * Responsible for parsing an HTML document.
32
 */
33
public final class HtmlPreview extends SwingNode {
34
35
  /**
36
   * The order is important: Swing factory will replace SVG images with
37
   * a blank image, which will cause the chained factory to cache the image
38
   * and exit. Instead, the SVG must execute first to rasterize the content.
39
   * Consequently, the chained factory must maintain insertion order.
40
   */
41
  private static final ChainedReplacedElementFactory FACTORY
42
    = new ChainedReplacedElementFactory(
43
    new SvgReplacedElementFactory(),
44
    new SwingReplacedElementFactory()
45
  );
46
47
  /**
48
   * Used to populate the {@link #HTML_HEAD} with stylesheet file references.
49
   */
50
  private static final String HTML_STYLESHEET =
51
    "<link rel='stylesheet' href='%s'>";
52
53
  private static final String HTML_BASE =
54
    "<base href='%s'>";
55
56
  /**
57
   * Render CSS using points (pt) not pixels (px) to reduce the chance of
58
   * poor rendering. The {@link #generateHead()} method fills placeholders.
59
   * When the user has not set a locale, only one stylesheet is added to
60
   * the document. In order, the placeholders are as follows:
61
   * <ol>
62
   * <li>%s --- language</li>
63
   * <li>%s --- default stylesheet</li>
64
   * <li>%s --- language-specific stylesheet</li>
65
   * <li>%s --- font family</li>
66
   * <li>%d --- font size (must be pixels, not points due to bug)</li>
67
   * <li>%s --- base href</li>
68
   * </p>
69
   */
70
  private static final String HTML_HEAD =
71
    """
72
      <!doctype html>
73
      <html lang='%s'><head><title> </title><meta charset='utf-8'>
74
      %s%s<style>body{font-family:'%s';font-size: %dpx;}</style>%s</head><body>
75
      """;
76
77
  private static final String HTML_TAIL = "</body></html>";
78
79
  private static final URL HTML_STYLE_PREVIEW = toUrl( STYLESHEET_PREVIEW );
80
81
  /**
82
   * Reusing this buffer prevents repetitious memory re-allocations.
83
   */
84
  private final StringBuilder mDocument = new StringBuilder( 65536 );
85
86
  private HtmlPanel mView;
87
  private JScrollPane mScrollPane;
88
  private String mBaseUriPath = "";
89
  private String mHead = "";
90
91
  private final Workspace mWorkspace;
92
93
  /**
94
   * Creates a new preview pane that can scroll to the caret position within the
95
   * document.
96
   *
97
   * @param workspace Contains locale and font size information.
98
   */
99
  public HtmlPreview( final Workspace workspace ) {
100
    mWorkspace = workspace;
101
102
    // Attempts to prevent a flash of black un-styled content upon load.
103
    setStyle( "-fx-background-color: white;" );
104
105
    invokeLater( () -> {
106
      mHead = generateHead();
107
      mView = new HtmlPanel();
108
      mScrollPane = new JScrollPane( mView );
109
110
      // Enabling the cache attempts to prevent black flashes when resizing.
111
      setCache( true );
112
      setCacheHint( SPEED );
113
      setContent( mScrollPane );
114
115
      final var context = mView.getSharedContext();
116
      final var textRenderer = context.getTextRenderer();
117
      context.setReplacedElementFactory( FACTORY );
118
      textRenderer.setSmoothingThreshold( 0 );
119
120
      localeProperty().addListener( ( c, o, n ) -> rerender() );
121
      fontFamilyProperty().addListener( ( c, o, n ) -> rerender() );
122
      fontSizeProperty().addListener( ( c, o, n ) -> rerender() );
123
    } );
124
  }
125
126
  /**
127
   * Updates the internal HTML source shown in the preview pane.
128
   *
129
   * @param html The new HTML document to display.
130
   */
131
  public void render( final String html ) {
132
    mView.render( decorate( html ), getBaseUri() );
133
  }
134
135
  /**
136
   * Clears the caches then rerenders the content.
137
   */
138
  public void refresh() {
139
    FACTORY.clearCache();
140
    rerender();
141
  }
142
143
  /**
144
   * Recomputes the HTML head then renders the document.
145
   */
146
  private void rerender() {
147
    mHead = generateHead();
148
    render( mDocument.toString() );
149
  }
150
151
  /**
152
   * Attaches the HTML head prefix and HTML tail suffix to the given HTML
153
   * string.
154
   *
155
   * @param html The HTML to adorn with opening and closing tags.
156
   * @return A complete HTML document, ready for rendering.
157
   */
158
  private String decorate( final String html ) {
159
    mDocument.setLength( 0 );
160
    mDocument.append( html );
161
162
    // Head and tail must be separate from document due to re-rendering.
163
    return mHead + mDocument.toString() + HTML_TAIL;
164
  }
165
166
  /**
167
   * Called when settings are changed that affect the HTML document preamble.
168
   * This is a minor performance optimization to avoid generating the head
169
   * each time that the document itself changes.
170
   *
171
   * @return A new doctype and HTML {@code head} element.
172
   */
173
  private String generateHead() {
174
    final var locale = getLocale();
175
    final var url = toUrl( locale );
176
    final var base = getBaseUri();
177
178
    // Point sizes are converted to pixels because of a rendering bug.
179
    return format(
180
      HTML_HEAD,
181
      locale.getLanguage(),
182
      format( HTML_STYLESHEET, HTML_STYLE_PREVIEW ),
183
      url == null ? "" : format( HTML_STYLESHEET, url ),
184
      getFontFamily(),
185
      (int) (getFontSize() * (1 + 1 / 3f)),
186
      base.isBlank() ? "" : format( HTML_BASE, base )
187
    );
188
  }
189
190
  /**
191
   * Clears the preview pane by rendering an empty string.
192
   */
193
  public void clear() {
194
    render( "" );
195
  }
196
197
  /**
198
   * Sets the base URI to the containing directory the file being edited.
199
   *
200
   * @param path The path to the file being edited.
201
   */
202
  public void setBaseUri( final Path path ) {
203
    final var parent = path.getParent();
204
    mBaseUriPath = parent == null ? "" : parent.toUri().toString();
205
  }
206
207
  /**
208
   * Scrolls to the closest element matching the given identifier without
209
   * waiting for the document to be ready.
210
   *
211
   * @param id Scroll the preview pane to this unique paragraph identifier.
212
   */
213
  public void scrollTo( final String id ) {
214
    final Runnable scrollToBox = () -> {
215
      int iter = 0;
216
      Box box = null;
217
218
      while( iter++ < 3 && ((box = mView.getBoxById( id )) == null) ) {
219
        try {
220
          sleep( 10 );
221
        } catch( final Exception ex ) {
222
          clue( ex );
223
        }
224
      }
225
226
      scrollTo( box );
227
    };
228
229
    if( Platform.isFxApplicationThread() ) {
230
      scrollToBox.run();
231
    }
232
    else {
233
      runLater( scrollToBox );
234
    }
235
  }
236
237
  /**
238
   * Scrolls to the location specified by the {@link Box} that corresponds
239
   * to a point somewhere in the preview pane. If there is no caret, then
240
   * this will not change the scroll position. Changing the scroll position
241
   * to the top if the {@link Box} instance is {@code null} will result in
242
   * jumping around a lot and inconsistent synchronization issues.
243
   *
244
   * @param box The rectangular region containing the caret, or {@code null}
245
   *            if the HTML does not have a caret.
246
   */
247
  private void scrollTo( final Box box ) {
248
    if( box != null ) {
249
      scrollTo( createPoint( box ) );
250
    }
251
  }
252
253
  private void scrollTo( final Point point ) {
254
    invokeLater( () -> {
255
      mView.scrollTo( point );
256
      getScrollPane().repaint();
257
    } );
258
  }
259
260
  /**
261
   * Creates a {@link Point} to use as a reference for scrolling to the area
262
   * described by the given {@link Box}. The {@link Box} coordinates are used
263
   * to populate the {@link Point}'s location, with minor adjustments for
264
   * vertical centering.
265
   *
266
   * @param box The {@link Box} that represents a scrolling anchor reference.
267
   * @return A coordinate suitable for scrolling to.
268
   */
269
  private Point createPoint( final Box box ) {
270
    assert box != null;
271
272
    // Scroll back up by half the height of the scroll bar to keep the typing
273
    // area within the view port. Otherwise the view port will have jumped too
274
    // high up and the most recently typed letters won't be visible.
275
    int y = max( box.getAbsY() - getVerticalScrollBarHeight() / 2, 0 );
276
    int x = box.getAbsX();
277
278
    if( !box.getStyle().isInline() ) {
279
      final var margin = box.getMargin( mView.getLayoutContext() );
280
      y += margin.top();
281
      x += margin.left();
282
    }
283
284
    return new Point( x, y );
285
  }
286
287
  private String getBaseUri() {
288
    return mBaseUriPath;
289
  }
290
291
  private JScrollPane getScrollPane() {
292
    return mScrollPane;
293
  }
294
295
  public JScrollBar getVerticalScrollBar() {
296
    return getScrollPane().getVerticalScrollBar();
297
  }
298
299
  private int getVerticalScrollBarHeight() {
300
    return getVerticalScrollBar().getHeight();
301
  }
302
303
  /**
304
   * Returns the ISO 639 alpha-2 or alpha-3 language code followed by a hyphen
305
   * followed by the ISO 15924 alpha-4 script code, followed by an ISO 3166
306
   * alpha-2 country code or UN M.49 numeric-3 area code. For example, this
307
   * could return "en-Latn-CA" for Canadian English written in the Latin
308
   * character set.
309
   *
310
   * @return Unique identifier for language and country.
311
   */
312
  private static URL toUrl( final Locale locale ) {
313
    return toUrl(
314
      get(
315
        sSettings.getSetting( STYLESHEET_PREVIEW_LOCALE, "" ),
316
        locale.getLanguage(),
317
        locale.getScript(),
318
        locale.getCountry()
319
      )
320
    );
321
  }
322
323
  private static URL toUrl( final String path ) {
324
    return HtmlPreview.class.getResource( path );
325
  }
326
327
  private Locale getLocale() {
328
    return localeProperty().toLocale();
329
  }
330
331
  private LocaleProperty localeProperty() {
332
    return mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
333
  }
334
335
  private String getFontFamily() {
336
    return fontFamilyProperty().get();
337
  }
338
339
  private StringProperty fontFamilyProperty() {
340
    return mWorkspace.stringProperty( KEY_UI_FONT_PREVIEW_NAME );
341
  }
342
343
  private double getFontSize() {
344
    return fontSizeProperty().get();
345
  }
346
347
  /**
348
   * Returns the font size in points.
349
   *
339350
   * @return The user-defined font size (in pt).
340351
   */
A src/main/java/com/keenwrite/ui/tree/AltTreeCell.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.tree;
3
4
import javafx.beans.property.Property;
5
import javafx.beans.property.SimpleStringProperty;
6
import javafx.beans.value.ChangeListener;
7
import javafx.beans.value.ObservableValue;
8
import javafx.event.EventHandler;
9
import javafx.scene.control.TextField;
10
import javafx.scene.control.cell.TextFieldTreeCell;
11
import javafx.scene.input.KeyEvent;
12
import javafx.util.StringConverter;
13
14
import static javafx.application.Platform.runLater;
15
import static javafx.scene.input.KeyCode.ENTER;
16
import static javafx.scene.input.KeyCode.TAB;
17
import static javafx.scene.input.KeyEvent.KEY_RELEASED;
18
19
/**
20
 * Responsible for enhancing the existing cell behaviour with fairly common
21
 * functionality, including commit on focus loss and Enter to commit.
22
 *
23
 * @param <T> The type of data stored by the tree.
24
 */
25
public class AltTreeCell<T> extends TextFieldTreeCell<T> {
26
  private final KeyHandler mKeyHandler = new KeyHandler();
27
  private final Property<String> mInputText = new SimpleStringProperty();
28
  private FocusListener mFocusListener;
29
30
  public AltTreeCell( final StringConverter<T> converter ) {
31
    super( converter );
32
    assert converter != null;
33
34
    // When the text field is added as the graphics context, we hook into
35
    // the changed value to get a handle on the text field. From there it is
36
    // possible to add change the keyboard and focus behaviours.
37
    graphicProperty().addListener( ( c, o, n ) -> {
38
      if( o instanceof TextField ) {
39
        o.removeEventHandler( KEY_RELEASED, mKeyHandler );
40
        o.focusedProperty().removeListener( mFocusListener );
41
      }
42
43
      if( n instanceof TextField ) {
44
        n.addEventFilter( KEY_RELEASED, mKeyHandler );
45
        final var input = (TextField) n;
46
        mInputText.bind( input.textProperty() );
47
        mFocusListener = new FocusListener( input );
48
        n.focusedProperty().addListener( mFocusListener );
49
      }
50
    } );
51
  }
52
53
  private void commitEdit() {
54
    commitEdit( getConverter().fromString( mInputText.getValue() ) );
55
  }
56
57
  /**
58
   * Responsible for accepting the text when users press the Enter or Tab key.
59
   */
60
  private class KeyHandler implements EventHandler<KeyEvent> {
61
    @Override
62
    public void handle( final KeyEvent event ) {
63
      if( event.getCode() == ENTER || event.getCode() == TAB ) {
64
        commitEdit();
65
        event.consume();
66
      }
67
    }
68
  }
69
70
  /**
71
   * Responsible for committing edits when focus is lost. This will also
72
   * deselect the input field when focus is gained so that typing text won't
73
   * overwrite the entire existing text.
74
   */
75
  private class FocusListener implements ChangeListener<Boolean> {
76
    private final TextField mInput;
77
78
    private FocusListener( final TextField input ) {
79
      mInput = input;
80
    }
81
82
    @Override
83
    public void changed(
84
      final ObservableValue<? extends Boolean> c,
85
      final Boolean endedFocus, final Boolean beganFocus ) {
86
87
      if( beganFocus ) {
88
        runLater( mInput::deselect );
89
      }
90
      else if( endedFocus ) {
91
        commitEdit();
92
      }
93
    }
94
  }
95
}
196
A src/main/java/com/keenwrite/ui/tree/AltTreeCellFactory.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.tree;
3
4
import javafx.scene.control.TreeCell;
5
import javafx.scene.control.TreeView;
6
import javafx.util.Callback;
7
import javafx.util.StringConverter;
8
9
//import javafx.collections.ObservableList;
10
//import javafx.scene.control.TreeCell;
11
//import javafx.scene.control.TreeItem;
12
//import javafx.scene.control.TreeView;
13
//import javafx.scene.input.ClipboardContent;
14
//import javafx.scene.input.DataFormat;
15
//import javafx.scene.input.DragEvent;
16
//import javafx.scene.input.MouseEvent;
17
//import javafx.util.StringConverter;
18
//import java.util.Objects;
19
//import static javafx.scene.input.TransferMode.MOVE;
20
21
/**
22
 * Responsible for creating new {@link TreeCell} instances.
23
 *
24
 * @param <T> The data type stored in the tree.
25
 */
26
public class AltTreeCellFactory<T>
27
  implements Callback<TreeView<T>, TreeCell<T>> {
28
  private final StringConverter<T> mConverter;
29
30
  public AltTreeCellFactory( final StringConverter<T> converter ) {
31
    mConverter = converter;
32
  }
33
34
  @Override
35
  public TreeCell<T> call( final TreeView<T> treeView ) {
36
    return new AltTreeCell<>( mConverter );
37
  }
38
39
//  private static final String STYLE_CLASS_DROP_TARGET = "drop-target";
40
//  private static final DataFormat JAVA_FORMAT =
41
//      new DataFormat( APP_JAVA_OBJECT.toString() );
42
//
43
//  private TreeItem<String> mDraggedTreeItem;
44
//  private TreeCell<String> mTargetCell;
45
//
46
//  @Override
47
//  public TreeCell<String> call( final TreeView<String> treeView ) {
48
//    final var cell = createTreeCell();
49
//
50
//    cell.setOnDragDetected( event -> dragDetected( event, cell ) );
51
//    cell.setOnDragOver( event -> dragOver( event, cell ) );
52
//    cell.setOnDragDropped( event -> dragDropped( event, cell, treeView ) );
53
//    cell.setOnDragDone( event -> dragClear() );
54
//
55
//    return cell;
56
//  }
57
//
58
//  private TreeCell<String> createTreeCell() {
59
//  }
60
//
61
//  /**
62
//   * Drag start.
63
//   *
64
//   * @param event    The drag start {@link MouseEvent}.
65
//   * @param treeCell The cell being dragged.
66
//   */
67
//private void dragDetected(
68
//  final MouseEvent event, final TreeCell<String> treeCell ) {
69
//  final var sourceItem = treeCell.getTreeItem();
70
//
71
//  // Prevent dragging the root item.
72
//  if( sourceItem != null && sourceItem.getParent() != null ) {
73
//    final var dragboard = treeCell.startDragAndDrop( MOVE );
74
//    final var clipboard = new ClipboardContent();
75
//    clipboard.put( JAVA_FORMAT, sourceItem.getValue() );
76
//    dragboard.setContent( clipboard );
77
//    dragboard.setDragView( treeCell.snapshot( null, null ) );
78
//    event.consume();
79
//
80
//    mDraggedTreeItem = sourceItem;
81
//  }
82
//}
83
//
84
//  /**
85
//   * Drag over another {@link TreeCell} instance.
86
//   *
87
//   * @param event    The drag over {@link DragEvent}.
88
//   * @param treeCell The cell dragged over.
89
//   * @throws IllegalStateException Drag transfer "move" mode denied.
90
//   */
91
//  private void dragOver(
92
//    final DragEvent event, final TreeCell<String> treeCell ) {
93
//    if( event.getDragboard().hasContent( JAVA_FORMAT ) ) {
94
//      final var thisItem = treeCell.getTreeItem();
95
//
96
//      if( mDraggedTreeItem == null ||
97
//        thisItem == null ||
98
//        thisItem == mDraggedTreeItem ) {
99
//        return;
100
//      }
101
//
102
//      // Ignore dragging over the root item.
103
//      if( mDraggedTreeItem.getParent() == null ) {
104
//        dragClear();
105
//        return;
106
//      }
107
//
108
//      event.acceptTransferModes( MOVE );
109
//
110
//      if( !Objects.equals( mTargetCell, treeCell ) ) {
111
//        dragClear();
112
//        mTargetCell = treeCell;
113
//        mTargetCell.getStyleClass().add( STYLE_CLASS_DROP_TARGET );
114
//      }
115
//    }
116
//  }
117
//
118
//  /**
119
//   * Dragged item is dropped
120
//   *
121
//   * @param event    The drag dropped {@link DragEvent}.
122
//   * @param treeCell The cell dropped onto.
123
//   */
124
//  private void dragDropped( final DragEvent event,
125
//                            final TreeCell<String> treeCell,
126
//                            final TreeView<String> treeView ) {
127
//    if( !event.getDragboard().hasContent( JAVA_FORMAT ) ) {
128
//      return;
129
//    }
130
//
131
//    final var sourceItem = mDraggedTreeItem;
132
//    final var sourceItemParent = mDraggedTreeItem.getParent();
133
//    final var targetItem = treeCell.getTreeItem();
134
//    final var targetItemParent = targetItem.getParent();
135
//
136
//    sourceItemParent.getChildren().remove( sourceItem );
137
//
138
//    final ObservableList<TreeItem<String>> children;
139
//    final int index;
140
//
141
//    // Dropping onto a parent node makes the source item the first child.
142
//    if( Objects.equals( sourceItemParent, targetItem ) ) {
143
//      children = targetItem.getChildren();
144
//      index = 0;
145
//    }
146
//    else if( targetItemParent != null) {
147
//      children = targetItemParent.getChildren();
148
//      index = children.indexOf( targetItem ) + 1;
149
//    }
150
//    else {
151
//      children = sourceItemParent.getChildren();
152
//      index = 0;
153
//    }
154
//
155
//    children.add( index, sourceItem );
156
//
157
//    treeView.getSelectionModel().clearSelection();
158
//    treeView.getSelectionModel().select( sourceItem );
159
//
160
//    // TODO: Notify a listener of the old and new tree item position.
161
//
162
//    event.setDropCompleted( true );
163
//  }
164
//
165
//  private void dragClear() {
166
//    final var targetCell = mTargetCell;
167
//
168
//    if( targetCell != null ) {
169
//      targetCell.getStyleClass().remove( STYLE_CLASS_DROP_TARGET );
170
//    }
171
//  }
172
}
1173
A src/main/java/com/keenwrite/ui/tree/AltTreeView.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.tree;
3
4
import javafx.scene.control.TreeItem;
5
import javafx.scene.control.TreeView;
6
import javafx.util.StringConverter;
7
8
/**
9
 * Responsible for allowing users to edit items in the tree as well as
10
 * drag and drop. The goal is to be a drop-in replacement for the regular
11
 * JavaFX {@link TreeView} that does not offer editing and moving {@link
12
 * TreeItem} instances.
13
 *
14
 * @param <T> The type of data to edit.
15
 */
16
public class AltTreeView<T> extends TreeView<T> {
17
  public AltTreeView(
18
    final TreeItem<T> root, final StringConverter<T> converter ) {
19
    super( root );
20
21
    setEditable( true );
22
    setCellFactory( new AltTreeCellFactory<>( converter ) );
23
    setShowRoot( false );
24
25
    // When focus is lost, clear the selected item only when not editing.
26
    focusedProperty().addListener( ( c, o, n ) -> {
27
      if( o && getEditingItem() == null ) {
28
        getSelectionModel().clearSelection();
29
      }
30
    } );
31
  }
32
}
133
A src/main/java/com/keenwrite/ui/tree/TreeItemConverter.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.tree;
3
4
import javafx.util.StringConverter;
5
6
/**
7
 * Responsible for converting objects to and from string instances. The
8
 * tree items contain only strings, so this effectively is a string-to-string
9
 * converter, which allows the implementation to retain its generics.
10
 */
11
public class TreeItemConverter extends StringConverter<String> {
12
13
  @Override
14
  public String toString( final String object ) {
15
    return sanitize( object );
16
  }
17
18
  @Override
19
  public String fromString( final String string ) {
20
    return sanitize( string );
21
  }
22
23
  private String sanitize( final String string ) {
24
    return string == null ? "" : string;
25
  }
26
}
127
M src/main/resources/com/keenwrite/messages.properties
1313
Main.menu.insert=_Insert
1414
Main.menu.format=Forma_t
15
Main.menu.definition=_Definition
16
Main.menu.view=_View
17
Main.menu.help=_Help
18
19
# ########################################################################
20
# Detachable Tabs
21
# ########################################################################
22
23
# {0} is the application title; {1} is a unique window ID.
24
Detach.tab.title={0} - {1}
25
26
# ########################################################################
27
# Status Bar
28
# ########################################################################
29
30
Main.status.text.offset=offset
31
Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2}
32
Main.status.state.default=OK
33
Main.status.export.success=Saved as {0}
34
35
Main.status.error.bootstrap.eval=Note: Bootstrap definition of ''{0}'' not found
36
37
Main.status.error.parse={0} (near ${Main.status.text.offset} {1})
38
Main.status.error.def.blank=Move the caret to a word before inserting a definition
39
Main.status.error.def.empty=Create a definition before inserting a definition
40
Main.status.error.def.missing=No definition value found for ''{0}''
41
Main.status.error.r=Error with [{0}...]: {1}
42
Main.status.error.file.missing=Not found: {0}
43
44
Main.status.error.messages.recursion=Lookup depth exceeded, check for loops in ''{0}''
45
Main.status.error.messages.syntax=Missing ''}'' in ''{0}''
46
47
Main.status.error.undo=Cannot undo; beginning of undo history reached
48
Main.status.error.redo=Cannot redo; end of redo history reached
49
50
Main.status.image.request.init=Initializing HTTP request
51
Main.status.image.request.fetch=Requesting content type from {0}
52
Main.status.image.request.success=Determined content type ''{0}''
53
Main.status.image.request.error.media=No media type for ''{0}''
54
Main.status.image.request.error.cert=Could not accept certificate for ''{0}''
55
56
Main.status.font.search.missing=No font name starting with ''{0}'' was found
57
58
# ########################################################################
59
# Search Bar
60
# ########################################################################
61
62
Main.search.stop.tooltip=Close search bar
63
Main.search.stop.icon=CLOSE
64
Main.search.next.tooltip=Find next match
65
Main.search.next.icon=CHEVRON_DOWN
66
Main.search.prev.tooltip=Find previous match
67
Main.search.prev.icon=CHEVRON_UP
68
Main.search.find.tooltip=Search document for text
69
Main.search.find.icon=SEARCH
70
Main.search.match.none=No matches
71
Main.search.match.some={0} of {1} matches
72
73
# ########################################################################
74
# Workspace preferences
75
# ########################################################################
76
77
workspace.r=R
78
workspace.r.script=Startup Script
79
workspace.r.script.desc=Script runs prior to executing R statements within the document.
80
workspace.r.dir=Working Directory
81
workspace.r.dir.desc=Value assigned to {0}application.r.working.directory{1} and usable in the startup script.
82
workspace.r.dir.title=Directory
83
workspace.r.delimiter.began=Delimiter Prefix
84
workspace.r.delimiter.began.desc=Prefix of expression that wraps inserted definitions.
85
workspace.r.delimiter.began.title=Opening
86
workspace.r.delimiter.ended=Delimiter Suffix
87
workspace.r.delimiter.ended.desc=Suffix of expression that wraps inserted definitions.
88
workspace.r.delimiter.ended.title=Closing
89
90
workspace.images=Images
91
workspace.images.dir=Absolute Directory
92
workspace.images.dir.desc=Path to search for local file system images.
93
workspace.images.dir.title=Directory
94
workspace.images.order=Extensions
95
workspace.images.order.desc=Preferred order of image file types to embed, separated by spaces.
96
workspace.images.order.title=Extensions
97
98
workspace.definition=Definition
99
workspace.definition.path=File name
100
workspace.definition.path.desc=Absolute path to interpolated string definition.
101
workspace.definition.path.title=Path
102
workspace.definition.delimiter.began=Delimiter Prefix
103
workspace.definition.delimiter.began.desc=Indicates when a definition key is starting.
104
workspace.definition.delimiter.began.title=Opening
105
workspace.definition.delimiter.ended=Delimiter Suffix
106
workspace.definition.delimiter.ended.desc=Indicates when a definition key is ending.
107
workspace.definition.delimiter.ended.title=Closing
108
109
workspace.ui.theme=Themes
110
workspace.ui.theme.selection=Bundled
111
workspace.ui.theme.selection.desc=Pre-packaged application style (default: Modena Light)
112
workspace.ui.theme.selection.title=Name
113
workspace.ui.theme.custom=Custom
114
workspace.ui.theme.custom.desc=User-defined JavaFX cascading stylesheet file
115
workspace.ui.theme.custom.title=Path
116
117
workspace.ui.font=Fonts
118
workspace.ui.font.editor=Editor Font
119
workspace.ui.font.editor.name=Name
120
workspace.ui.font.editor.name.desc=Text editor font name (sans-serif font recommended).
121
workspace.ui.font.editor.name.title=Family
122
workspace.ui.font.editor.size=Size
123
workspace.ui.font.editor.size.desc=Font size.
124
workspace.ui.font.editor.size.title=Points
125
workspace.ui.font.preview=Preview Font
126
workspace.ui.font.preview.name=Name
127
workspace.ui.font.preview.name.desc=Preview pane font name (must support ligatures, serif font recommended).
128
workspace.ui.font.preview.name.title=Family
129
workspace.ui.font.preview.size=Size
130
workspace.ui.font.preview.size.desc=Font size.
131
workspace.ui.font.preview.size.title=Points
132
workspace.ui.font.preview.mono.name=Name
133
workspace.ui.font.preview.mono.name.desc=Monospace font name.
134
workspace.ui.font.preview.mono.name.title=Family
135
workspace.ui.font.preview.mono.size=Size
136
workspace.ui.font.preview.mono.size.desc=Monospace font size.
137
workspace.ui.font.preview.mono.size.title=Points
138
139
workspace.language=Language
140
workspace.language.locale=Internationalization
141
workspace.language.locale.desc=Language for application and HTML export.
142
workspace.language.locale.title=Locale
143
144
# ########################################################################
145
# Definition Pane and its Tree View
146
# ########################################################################
147
148
Definition.menu.add.default=Undefined
149
150
# ########################################################################
151
# Definition Pane
152
# ########################################################################
153
154
Pane.definition.node.root.title=Definitions
155
156
# ########################################################################
157
# Failure messages with respect to YAML files.
158
# ########################################################################
159
160
yaml.error.open=Could not open YAML file (ensure non-empty file).
161
yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''.
162
yaml.error.missing=Empty definition value for key ''{0}''.
163
yaml.error.tree.form=Unassigned definition near ''{0}''.
164
165
# ########################################################################
166
# Text Resource
167
# ########################################################################
168
169
TextResource.load.error.unsaved=The file ''{0}'' is unsaved or does not exist.
170
TextResource.load.error.permissions=The file ''{0}'' must be readable and writable.
171
172
# ########################################################################
173
# Text Resources
174
# ########################################################################
175
176
TextResource.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
177
TextResource.saveFailed.title=Save
178
179
# ########################################################################
180
# File Open
181
# ########################################################################
182
183
Dialog.file.choose.open.title=Open File
184
Dialog.file.choose.save.title=Save File
185
Dialog.file.choose.export.title=Export File
186
187
Dialog.file.choose.filter.title.source=Source Files
188
Dialog.file.choose.filter.title.definition=Definition Files
189
Dialog.file.choose.filter.title.xml=XML Files
190
Dialog.file.choose.filter.title.all=All Files
191
192
# ########################################################################
193
# Browse File
194
# ########################################################################
195
196
BrowseFileButton.chooser.title=Browse for local file
197
BrowseFileButton.chooser.allFilesFilter=All Files
198
BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
199
200
# ########################################################################
201
# Alert Dialog
202
# ########################################################################
203
204
Alert.file.close.title=Close
205
Alert.file.close.text=Save changes to {0}?
206
207
# ########################################################################
208
# Image Dialog
209
# ########################################################################
210
211
Dialog.image.title=Image
212
Dialog.image.chooser.imagesFilter=Images
213
Dialog.image.previewLabel.text=Markdown Preview\:
214
Dialog.image.textLabel.text=Alternate Text\:
215
Dialog.image.titleLabel.text=Title (tooltip)\:
216
Dialog.image.urlLabel.text=Image URL\:
217
218
# ########################################################################
219
# Hyperlink Dialog
220
# ########################################################################
221
222
Dialog.link.title=Link
223
Dialog.link.previewLabel.text=Markdown Preview\:
224
Dialog.link.textLabel.text=Link Text\:
225
Dialog.link.titleLabel.text=Title (tooltip)\:
226
Dialog.link.urlLabel.text=Link URL\:
227
228
# ########################################################################
229
# About Dialog
230
# ########################################################################
231
232
Dialog.about.title=About {0}
233
Dialog.about.header={0}
234
Dialog.about.content=Copyright 2016-{0} White Magic Software, Ltd.\n\nVersion: {1}
235
236
# ########################################################################
237
# Application Actions
238
# ########################################################################
239
240
App.action.file.new.description=Create a new file
241
App.action.file.new.accelerator=Shortcut+N
242
App.action.file.new.icon=FILE_ALT
243
App.action.file.new.text=_New
244
245
App.action.file.open.description=Open a new file
246
App.action.file.open.accelerator=Shortcut+O
247
App.action.file.open.text=_Open...
248
App.action.file.open.icon=FOLDER_OPEN_ALT
249
250
App.action.file.close.description=Close the current document
251
App.action.file.close.accelerator=Shortcut+W
252
App.action.file.close.text=_Close
253
254
App.action.file.close_all.description=Close all open documents
255
App.action.file.close_all.accelerator=Ctrl+F4
256
App.action.file.close_all.text=Close All
257
258
App.action.file.save.description=Save the document
259
App.action.file.save.accelerator=Shortcut+S
260
App.action.file.save.text=_Save
261
App.action.file.save.icon=FLOPPY_ALT
262
263
App.action.file.save_as.description=Rename the current document
264
App.action.file.save_as.text=Save _As
265
266
App.action.file.save_all.description=Save all open documents
267
App.action.file.save_all.accelerator=Shortcut+Shift+S
268
App.action.file.save_all.text=Save A_ll
269
270
App.action.file.export.html_svg.description=Export the current document as HTML + SVG
271
App.action.file.export.text=_Export As
272
App.action.file.export.html_svg.text=HTML and S_VG
273
274
App.action.file.export.html_tex.description=Export the current document as HTML + TeX
275
App.action.file.export.html_tex.text=HTML and _TeX
276
277
App.action.file.export.markdown.description=Export the current document as Markdown
278
App.action.file.export.markdown.text=Markdown
279
280
App.action.file.exit.description=Quit the application
281
App.action.file.exit.text=E_xit
282
283
284
App.action.edit.undo.description=Undo the previous edit
285
App.action.edit.undo.accelerator=Shortcut+Z
286
App.action.edit.undo.text=_Undo
287
App.action.edit.undo.icon=UNDO
288
289
App.action.edit.redo.description=Redo the previous edit
290
App.action.edit.redo.accelerator=Shortcut+Y
291
App.action.edit.redo.text=_Redo
292
App.action.edit.redo.icon=REPEAT
293
294
App.action.edit.cut.description=Delete the selected text or line
295
App.action.edit.cut.accelerator=Shortcut+X
296
App.action.edit.cut.text=Cu_t
297
App.action.edit.cut.icon=CUT
298
299
App.action.edit.copy.description=Copy the selected text
300
App.action.edit.copy.accelerator=Shortcut+C
301
App.action.edit.copy.text=_Copy
302
App.action.edit.copy.icon=COPY
303
304
App.action.edit.paste.description=Paste from the clipboard
305
App.action.edit.paste.accelerator=Shortcut+V
306
App.action.edit.paste.text=_Paste
307
App.action.edit.paste.icon=PASTE
308
309
App.action.edit.select_all.description=Highlight the current document text
310
App.action.edit.select_all.accelerator=Shortcut+A
311
App.action.edit.select_all.text=Select _All
312
313
App.action.edit.find.description=Search for text in the document
314
App.action.edit.find.accelerator=Shortcut+F
315
App.action.edit.find.text=_Find
316
App.action.edit.find.icon=SEARCH
317
318
App.action.edit.find_next.description=Find next occurrence
319
App.action.edit.find_next.accelerator=F3
320
App.action.edit.find_next.text=Find _Next
321
322
App.action.edit.find_prev.description=Find previous occurrence
323
App.action.edit.find_prev.accelerator=Shift+F3
324
App.action.edit.find_prev.text=Find _Prev
325
326
App.action.edit.preferences.description=Edit user preferences
327
App.action.edit.preferences.accelerator=Ctrl+Alt+S
328
App.action.edit.preferences.text=_Preferences
329
330
331
App.action.format.bold.description=Insert strong text
332
App.action.format.bold.accelerator=Shortcut+B
333
App.action.format.bold.text=_Bold
334
App.action.format.bold.icon=BOLD
335
336
App.action.format.italic.description=Insert text emphasis
337
App.action.format.italic.accelerator=Shortcut+I
338
App.action.format.italic.text=_Italic
339
App.action.format.italic.icon=ITALIC
340
341
App.action.format.superscript.description=Insert superscript text
342
App.action.format.superscript.accelerator=Shortcut+[
343
App.action.format.superscript.text=Su_perscript
344
App.action.format.superscript.icon=SUPERSCRIPT
345
346
App.action.format.subscript.description=Insert subscript text
347
App.action.format.subscript.accelerator=Shortcut+]
348
App.action.format.subscript.text=Su_bscript
349
App.action.format.subscript.icon=SUBSCRIPT
350
351
App.action.format.strikethrough.description=Insert struck text
352
App.action.format.strikethrough.accelerator=Shortcut+T
353
App.action.format.strikethrough.text=Stri_kethrough
354
App.action.format.strikethrough.icon=STRIKETHROUGH
355
356
357
App.action.insert.blockquote.description=Insert blockquote
358
App.action.insert.blockquote.accelerator=Ctrl+Q
359
App.action.insert.blockquote.text=_Blockquote
360
App.action.insert.blockquote.icon=QUOTE_LEFT
361
362
App.action.insert.code.description=Insert inline code
363
App.action.insert.code.accelerator=Shortcut+K
364
App.action.insert.code.text=Inline _Code
365
App.action.insert.code.icon=CODE
366
367
App.action.insert.fenced_code_block.description=Insert code block
368
App.action.insert.fenced_code_block.accelerator=Shortcut+Shift+K
369
App.action.insert.fenced_code_block.text=_Fenced Code Block
370
App.action.insert.fenced_code_block.prompt.text=Enter code here
371
App.action.insert.fenced_code_block.icon=FILE_CODE_ALT
372
373
App.action.insert.link.description=Insert hyperlink
374
App.action.insert.link.accelerator=Shortcut+L
375
App.action.insert.link.text=_Link...
376
App.action.insert.link.icon=LINK
377
378
App.action.insert.image.description=Insert image
379
App.action.insert.image.accelerator=Shortcut+G
380
App.action.insert.image.text=_Image...
381
App.action.insert.image.icon=PICTURE_ALT
382
383
App.action.insert.heading.description=Insert heading level
384
App.action.insert.heading.accelerator=Shortcut+
385
App.action.insert.heading.icon=HEADER
386
387
App.action.insert.heading_1.description=${App.action.insert.heading.description} 1
388
App.action.insert.heading_1.accelerator=${App.action.insert.heading.accelerator}1
389
App.action.insert.heading_1.text=Heading _1
390
App.action.insert.heading_1.icon=${App.action.insert.heading.icon}
391
392
App.action.insert.heading_2.description=${App.action.insert.heading.description} 2
393
App.action.insert.heading_2.accelerator=${App.action.insert.heading.accelerator}2
394
App.action.insert.heading_2.text=Heading _2
395
App.action.insert.heading_2.icon=${App.action.insert.heading.icon}
396
397
App.action.insert.heading_3.description=${App.action.insert.heading.description} 3
398
App.action.insert.heading_3.accelerator=${App.action.insert.heading.accelerator}3
399
App.action.insert.heading_3.text=Heading _3
400
App.action.insert.heading_3.icon=${App.action.insert.heading.icon}
401
402
App.action.insert.unordered_list.description=Insert bulleted list
403
App.action.insert.unordered_list.accelerator=Shortcut+U
404
App.action.insert.unordered_list.text=_Unordered List
405
App.action.insert.unordered_list.icon=LIST_UL
406
407
App.action.insert.ordered_list.description=Insert enumerated list
408
App.action.insert.ordered_list.accelerator=Shortcut+Shift+O
409
App.action.insert.ordered_list.text=_Ordered List
410
App.action.insert.ordered_list.icon=LIST_OL
411
412
App.action.insert.horizontal_rule.description=Insert horizontal rule
413
App.action.insert.horizontal_rule.accelerator=Shortcut+H
414
App.action.insert.horizontal_rule.text=_Horizontal Rule
415
App.action.insert.horizontal_rule.icon=LIST_OL
416
417
418
App.action.definition.create.description=Create a new variable definition
419
App.action.definition.create.text=_Create
420
App.action.definition.create.icon=TREE
421
App.action.definition.create.tooltip=Add new item (Insert)
422
423
App.action.definition.rename.description=Rename the selected variable definition
424
App.action.definition.rename.text=_Rename
425
App.action.definition.rename.icon=EDIT
426
App.action.definition.rename.tooltip=Rename selected item (F2)
427
428
App.action.definition.delete.description=Delete the selected variable definitions
429
App.action.definition.delete.text=De_lete
430
App.action.definition.delete.icon=TRASH
431
App.action.definition.delete.tooltip=Delete selected items (Delete)
432
433
App.action.definition.insert.description=Insert a definition
15
Main.menu.definition=_Variable
16
Main.menu.view=Vie_w
17
Main.menu.help=_Help
18
19
# ########################################################################
20
# Detachable Tabs
21
# ########################################################################
22
23
# {0} is the application title; {1} is a unique window ID.
24
Detach.tab.title={0} - {1}
25
26
# ########################################################################
27
# Status Bar
28
# ########################################################################
29
30
Main.status.text.offset=offset
31
Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2}
32
Main.status.state.default=OK
33
Main.status.export.success=Saved as {0}
34
35
Main.status.error.bootstrap.eval=Note: Bootstrap variable of ''{0}'' not found
36
37
Main.status.error.parse={0} (near ${Main.status.text.offset} {1})
38
Main.status.error.def.blank=Move the caret to a word before inserting a variable
39
Main.status.error.def.empty=Create a variable before inserting one
40
Main.status.error.def.missing=No variable value found for ''{0}''
41
Main.status.error.r=Error with [{0}...]: {1}
42
Main.status.error.file.missing=Not found: {0}
43
44
Main.status.error.messages.recursion=Lookup depth exceeded, check for loops in ''{0}''
45
Main.status.error.messages.syntax=Missing ''}'' in ''{0}''
46
47
Main.status.error.undo=Cannot undo; beginning of undo history reached
48
Main.status.error.redo=Cannot redo; end of redo history reached
49
50
Main.status.image.request.init=Initializing HTTP request
51
Main.status.image.request.fetch=Requesting content type from {0}
52
Main.status.image.request.success=Determined content type ''{0}''
53
Main.status.image.request.error.media=No media type for ''{0}''
54
Main.status.image.request.error.cert=Could not accept certificate for ''{0}''
55
56
Main.status.font.search.missing=No font name starting with ''{0}'' was found
57
58
# ########################################################################
59
# Search Bar
60
# ########################################################################
61
62
Main.search.stop.tooltip=Close search bar
63
Main.search.stop.icon=CLOSE
64
Main.search.next.tooltip=Find next match
65
Main.search.next.icon=CHEVRON_DOWN
66
Main.search.prev.tooltip=Find previous match
67
Main.search.prev.icon=CHEVRON_UP
68
Main.search.find.tooltip=Search document for text
69
Main.search.find.icon=SEARCH
70
Main.search.match.none=No matches
71
Main.search.match.some={0} of {1} matches
72
73
# ########################################################################
74
# Workspace preferences
75
# ########################################################################
76
77
workspace.r=R
78
workspace.r.script=Startup Script
79
workspace.r.script.desc=Script runs prior to executing R statements within the document.
80
workspace.r.dir=Working Directory
81
workspace.r.dir.desc=Value assigned to {0}application.r.working.directory{1} and usable in the startup script.
82
workspace.r.dir.title=Directory
83
workspace.r.delimiter.began=Delimiter Prefix
84
workspace.r.delimiter.began.desc=Prefix of expression that wraps inserted variables.
85
workspace.r.delimiter.began.title=Opening
86
workspace.r.delimiter.ended=Delimiter Suffix
87
workspace.r.delimiter.ended.desc=Suffix of expression that wraps inserted variables.
88
workspace.r.delimiter.ended.title=Closing
89
90
workspace.images=Images
91
workspace.images.dir=Absolute Directory
92
workspace.images.dir.desc=Path to search for local file system images.
93
workspace.images.dir.title=Directory
94
workspace.images.order=Extensions
95
workspace.images.order.desc=Preferred order of image file types to embed, separated by spaces.
96
workspace.images.order.title=Extensions
97
98
workspace.definition=Variable
99
workspace.definition.path=File name
100
workspace.definition.path.desc=Absolute path to interpolated string variables.
101
workspace.definition.path.title=Path
102
workspace.definition.delimiter.began=Delimiter Prefix
103
workspace.definition.delimiter.began.desc=Indicates when a variable name is starting.
104
workspace.definition.delimiter.began.title=Opening
105
workspace.definition.delimiter.ended=Delimiter Suffix
106
workspace.definition.delimiter.ended.desc=Indicates when a variable name is ending.
107
workspace.definition.delimiter.ended.title=Closing
108
109
workspace.ui.theme=Themes
110
workspace.ui.theme.selection=Bundled
111
workspace.ui.theme.selection.desc=Pre-packaged application style (default: Modena Light)
112
workspace.ui.theme.selection.title=Name
113
workspace.ui.theme.custom=Custom
114
workspace.ui.theme.custom.desc=User-defined JavaFX cascading stylesheet file
115
workspace.ui.theme.custom.title=Path
116
117
workspace.ui.font=Fonts
118
workspace.ui.font.editor=Editor Font
119
workspace.ui.font.editor.name=Name
120
workspace.ui.font.editor.name.desc=Text editor font name (sans-serif font recommended).
121
workspace.ui.font.editor.name.title=Family
122
workspace.ui.font.editor.size=Size
123
workspace.ui.font.editor.size.desc=Font size.
124
workspace.ui.font.editor.size.title=Points
125
workspace.ui.font.preview=Preview Font
126
workspace.ui.font.preview.name=Name
127
workspace.ui.font.preview.name.desc=Preview pane font name (must support ligatures, serif font recommended).
128
workspace.ui.font.preview.name.title=Family
129
workspace.ui.font.preview.size=Size
130
workspace.ui.font.preview.size.desc=Font size.
131
workspace.ui.font.preview.size.title=Points
132
workspace.ui.font.preview.mono.name=Name
133
workspace.ui.font.preview.mono.name.desc=Monospace font name.
134
workspace.ui.font.preview.mono.name.title=Family
135
workspace.ui.font.preview.mono.size=Size
136
workspace.ui.font.preview.mono.size.desc=Monospace font size.
137
workspace.ui.font.preview.mono.size.title=Points
138
139
workspace.language=Language
140
workspace.language.locale=Internationalization
141
workspace.language.locale.desc=Language for application and HTML export.
142
workspace.language.locale.title=Locale
143
144
# ########################################################################
145
# Definition Pane and its Tree View
146
# ########################################################################
147
148
Definition.menu.add.default=Undefined
149
150
# ########################################################################
151
# Variable Definitions Pane
152
# ########################################################################
153
154
Pane.definition.node.root.title=Variables
155
156
# ########################################################################
157
# HTML Preview Pane
158
# ########################################################################
159
160
Pane.preview.title=Preview
161
162
# ########################################################################
163
# Document Outline Pane
164
# ########################################################################
165
166
Pane.outline.title=Outline
167
168
# ########################################################################
169
# Failure messages with respect to YAML files.
170
# ########################################################################
171
172
yaml.error.open=Could not open YAML file (ensure non-empty file).
173
yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''.
174
yaml.error.missing=Empty variable value for key ''{0}''.
175
yaml.error.tree.form=Unassigned variable near ''{0}''.
176
177
# ########################################################################
178
# Text Resource
179
# ########################################################################
180
181
TextResource.load.error.unsaved=The file ''{0}'' is unsaved or does not exist.
182
TextResource.load.error.permissions=The file ''{0}'' must be readable and writable.
183
184
# ########################################################################
185
# Text Resources
186
# ########################################################################
187
188
TextResource.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
189
TextResource.saveFailed.title=Save
190
191
# ########################################################################
192
# File Open
193
# ########################################################################
194
195
Dialog.file.choose.open.title=Open File
196
Dialog.file.choose.save.title=Save File
197
Dialog.file.choose.export.title=Export File
198
199
Dialog.file.choose.filter.title.source=Source Files
200
Dialog.file.choose.filter.title.definition=Variable Files
201
Dialog.file.choose.filter.title.xml=XML Files
202
Dialog.file.choose.filter.title.all=All Files
203
204
# ########################################################################
205
# Browse File
206
# ########################################################################
207
208
BrowseFileButton.chooser.title=Browse for local file
209
BrowseFileButton.chooser.allFilesFilter=All Files
210
BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
211
212
# ########################################################################
213
# Alert Dialog
214
# ########################################################################
215
216
Alert.file.close.title=Close
217
Alert.file.close.text=Save changes to {0}?
218
219
# ########################################################################
220
# Image Dialog
221
# ########################################################################
222
223
Dialog.image.title=Image
224
Dialog.image.chooser.imagesFilter=Images
225
Dialog.image.previewLabel.text=Markdown Preview\:
226
Dialog.image.textLabel.text=Alternate Text\:
227
Dialog.image.titleLabel.text=Title (tooltip)\:
228
Dialog.image.urlLabel.text=Image URL\:
229
230
# ########################################################################
231
# Hyperlink Dialog
232
# ########################################################################
233
234
Dialog.link.title=Link
235
Dialog.link.previewLabel.text=Markdown Preview\:
236
Dialog.link.textLabel.text=Link Text\:
237
Dialog.link.titleLabel.text=Title (tooltip)\:
238
Dialog.link.urlLabel.text=Link URL\:
239
240
# ########################################################################
241
# About Dialog
242
# ########################################################################
243
244
Dialog.about.title=About {0}
245
Dialog.about.header={0}
246
Dialog.about.content=Copyright 2016-{0} White Magic Software, Ltd.\n\nVersion: {1}
247
248
# ########################################################################
249
# Application Actions
250
# ########################################################################
251
252
App.action.file.new.description=Create a new file
253
App.action.file.new.accelerator=Shortcut+N
254
App.action.file.new.icon=FILE_ALT
255
App.action.file.new.text=_New
256
257
App.action.file.open.description=Open a new file
258
App.action.file.open.accelerator=Shortcut+O
259
App.action.file.open.text=_Open...
260
App.action.file.open.icon=FOLDER_OPEN_ALT
261
262
App.action.file.close.description=Close the current document
263
App.action.file.close.accelerator=Shortcut+W
264
App.action.file.close.text=_Close
265
266
App.action.file.close_all.description=Close all open documents
267
App.action.file.close_all.accelerator=Ctrl+F4
268
App.action.file.close_all.text=Close All
269
270
App.action.file.save.description=Save the document
271
App.action.file.save.accelerator=Shortcut+S
272
App.action.file.save.text=_Save
273
App.action.file.save.icon=FLOPPY_ALT
274
275
App.action.file.save_as.description=Rename the current document
276
App.action.file.save_as.text=Save _As
277
278
App.action.file.save_all.description=Save all open documents
279
App.action.file.save_all.accelerator=Shortcut+Shift+S
280
App.action.file.save_all.text=Save A_ll
281
282
App.action.file.export.html_svg.description=Export the current document as HTML + SVG
283
App.action.file.export.text=_Export As
284
App.action.file.export.html_svg.text=HTML and S_VG
285
286
App.action.file.export.html_tex.description=Export the current document as HTML + TeX
287
App.action.file.export.html_tex.text=HTML and _TeX
288
289
App.action.file.export.markdown.description=Export the current document as Markdown
290
App.action.file.export.markdown.text=Markdown
291
292
App.action.file.exit.description=Quit the application
293
App.action.file.exit.text=E_xit
294
295
296
App.action.edit.undo.description=Undo the previous edit
297
App.action.edit.undo.accelerator=Shortcut+Z
298
App.action.edit.undo.text=_Undo
299
App.action.edit.undo.icon=UNDO
300
301
App.action.edit.redo.description=Redo the previous edit
302
App.action.edit.redo.accelerator=Shortcut+Y
303
App.action.edit.redo.text=_Redo
304
App.action.edit.redo.icon=REPEAT
305
306
App.action.edit.cut.description=Delete the selected text or line
307
App.action.edit.cut.accelerator=Shortcut+X
308
App.action.edit.cut.text=Cu_t
309
App.action.edit.cut.icon=CUT
310
311
App.action.edit.copy.description=Copy the selected text
312
App.action.edit.copy.accelerator=Shortcut+C
313
App.action.edit.copy.text=_Copy
314
App.action.edit.copy.icon=COPY
315
316
App.action.edit.paste.description=Paste from the clipboard
317
App.action.edit.paste.accelerator=Shortcut+V
318
App.action.edit.paste.text=_Paste
319
App.action.edit.paste.icon=PASTE
320
321
App.action.edit.select_all.description=Highlight the current document text
322
App.action.edit.select_all.accelerator=Shortcut+A
323
App.action.edit.select_all.text=Select _All
324
325
App.action.edit.find.description=Search for text in the document
326
App.action.edit.find.accelerator=Shortcut+F
327
App.action.edit.find.text=_Find
328
App.action.edit.find.icon=SEARCH
329
330
App.action.edit.find_next.description=Find next occurrence
331
App.action.edit.find_next.accelerator=F3
332
App.action.edit.find_next.text=Find _Next
333
334
App.action.edit.find_prev.description=Find previous occurrence
335
App.action.edit.find_prev.accelerator=Shift+F3
336
App.action.edit.find_prev.text=Find _Prev
337
338
App.action.edit.preferences.description=Edit user preferences
339
App.action.edit.preferences.accelerator=Ctrl+Alt+S
340
App.action.edit.preferences.text=_Preferences
341
342
343
App.action.format.bold.description=Insert strong text
344
App.action.format.bold.accelerator=Shortcut+B
345
App.action.format.bold.text=_Bold
346
App.action.format.bold.icon=BOLD
347
348
App.action.format.italic.description=Insert text emphasis
349
App.action.format.italic.accelerator=Shortcut+I
350
App.action.format.italic.text=_Italic
351
App.action.format.italic.icon=ITALIC
352
353
App.action.format.superscript.description=Insert superscript text
354
App.action.format.superscript.accelerator=Shortcut+[
355
App.action.format.superscript.text=Su_perscript
356
App.action.format.superscript.icon=SUPERSCRIPT
357
358
App.action.format.subscript.description=Insert subscript text
359
App.action.format.subscript.accelerator=Shortcut+]
360
App.action.format.subscript.text=Su_bscript
361
App.action.format.subscript.icon=SUBSCRIPT
362
363
App.action.format.strikethrough.description=Insert struck text
364
App.action.format.strikethrough.accelerator=Shortcut+T
365
App.action.format.strikethrough.text=Stri_kethrough
366
App.action.format.strikethrough.icon=STRIKETHROUGH
367
368
369
App.action.insert.blockquote.description=Insert blockquote
370
App.action.insert.blockquote.accelerator=Ctrl+Q
371
App.action.insert.blockquote.text=_Blockquote
372
App.action.insert.blockquote.icon=QUOTE_LEFT
373
374
App.action.insert.code.description=Insert inline code
375
App.action.insert.code.accelerator=Shortcut+K
376
App.action.insert.code.text=Inline _Code
377
App.action.insert.code.icon=CODE
378
379
App.action.insert.fenced_code_block.description=Insert code block
380
App.action.insert.fenced_code_block.accelerator=Shortcut+Shift+K
381
App.action.insert.fenced_code_block.text=_Fenced Code Block
382
App.action.insert.fenced_code_block.prompt.text=Enter code here
383
App.action.insert.fenced_code_block.icon=FILE_CODE_ALT
384
385
App.action.insert.link.description=Insert hyperlink
386
App.action.insert.link.accelerator=Shortcut+L
387
App.action.insert.link.text=_Link...
388
App.action.insert.link.icon=LINK
389
390
App.action.insert.image.description=Insert image
391
App.action.insert.image.accelerator=Shortcut+G
392
App.action.insert.image.text=_Image...
393
App.action.insert.image.icon=PICTURE_ALT
394
395
App.action.insert.heading.description=Insert heading level
396
App.action.insert.heading.accelerator=Shortcut+
397
App.action.insert.heading.icon=HEADER
398
399
App.action.insert.heading_1.description=${App.action.insert.heading.description} 1
400
App.action.insert.heading_1.accelerator=${App.action.insert.heading.accelerator}1
401
App.action.insert.heading_1.text=Heading _1
402
App.action.insert.heading_1.icon=${App.action.insert.heading.icon}
403
404
App.action.insert.heading_2.description=${App.action.insert.heading.description} 2
405
App.action.insert.heading_2.accelerator=${App.action.insert.heading.accelerator}2
406
App.action.insert.heading_2.text=Heading _2
407
App.action.insert.heading_2.icon=${App.action.insert.heading.icon}
408
409
App.action.insert.heading_3.description=${App.action.insert.heading.description} 3
410
App.action.insert.heading_3.accelerator=${App.action.insert.heading.accelerator}3
411
App.action.insert.heading_3.text=Heading _3
412
App.action.insert.heading_3.icon=${App.action.insert.heading.icon}
413
414
App.action.insert.unordered_list.description=Insert bulleted list
415
App.action.insert.unordered_list.accelerator=Shortcut+U
416
App.action.insert.unordered_list.text=_Unordered List
417
App.action.insert.unordered_list.icon=LIST_UL
418
419
App.action.insert.ordered_list.description=Insert enumerated list
420
App.action.insert.ordered_list.accelerator=Shortcut+Shift+O
421
App.action.insert.ordered_list.text=_Ordered List
422
App.action.insert.ordered_list.icon=LIST_OL
423
424
App.action.insert.horizontal_rule.description=Insert horizontal rule
425
App.action.insert.horizontal_rule.accelerator=Shortcut+H
426
App.action.insert.horizontal_rule.text=_Horizontal Rule
427
App.action.insert.horizontal_rule.icon=LIST_OL
428
429
430
App.action.definition.create.description=Create a new variable
431
App.action.definition.create.text=_Create
432
App.action.definition.create.icon=TREE
433
App.action.definition.create.tooltip=Add new item (Insert)
434
435
App.action.definition.rename.description=Rename the selected variable
436
App.action.definition.rename.text=_Rename
437
App.action.definition.rename.icon=EDIT
438
App.action.definition.rename.tooltip=Rename selected item (F2)
439
440
App.action.definition.delete.description=Delete the selected variables
441
App.action.definition.delete.text=De_lete
442
App.action.definition.delete.icon=TRASH
443
App.action.definition.delete.tooltip=Delete selected items (Delete)
444
445
App.action.definition.insert.description=Insert a variable
434446
App.action.definition.insert.accelerator=Ctrl+Space
435447
App.action.definition.insert.text=_Insert