Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M gradle.properties
11
org.gradle.jvmargs=-Xmx1G -XX:MaxPermSize=512m
2
org.gradle.daemon=true
3
org.gradle.parallel=true
24
35
M src/main/java/com/keenwrite/MainApp.java
55
import com.keenwrite.service.Snitch;
66
import javafx.application.Application;
7
import javafx.application.Platform;
87
import javafx.stage.Stage;
98
...
1716
import static javafx.scene.input.KeyCode.F11;
1817
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
19
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
2018
2119
/**
2220
 * Application entry point. The application allows users to edit plain text
2321
 * files in a markup notation and see a real-time preview of the formatted
2422
 * output.
2523
 */
26
@SuppressWarnings({"FieldCanBeLocal", "unused", "RedundantSuppression"})
24
@SuppressWarnings( {"FieldCanBeLocal", "unused", "RedundantSuppression"} )
2725
public final class MainApp extends Application {
2826
...
6765
6866
    stage.show();
69
  }
70
71
  /**
72
   * Saves the workspace then terminates the application.
73
   */
74
  @Override
75
  public void stop() {
76
    save();
77
    getSnitch().stop();
78
    Platform.exit();
79
    System.exit( 0 );
80
  }
81
82
  /**
83
   * Saves the current application state configuration and user preferences.
84
   */
85
  private void save() {
86
    mWorkspace.save();
8767
  }
8868
...
10787
  private void initStage( final Stage stage ) {
10888
    stage.setTitle( APP_TITLE );
109
    stage.addEventHandler( WINDOW_CLOSE_REQUEST, event -> stop() );
11089
    stage.addEventHandler( KEY_PRESSED, event -> {
11190
      if( F11.equals( event.getCode() ) ) {
M src/main/java/com/keenwrite/MainPane.java
2626
import com.panemu.tiwulfx.control.dock.DetachableTab;
2727
import com.panemu.tiwulfx.control.dock.DetachableTabPane;
28
import javafx.beans.property.*;
29
import javafx.collections.ListChangeListener;
30
import javafx.event.ActionEvent;
31
import javafx.event.Event;
32
import javafx.event.EventHandler;
33
import javafx.scene.Scene;
34
import javafx.scene.control.SplitPane;
35
import javafx.scene.control.Tab;
36
import javafx.scene.control.Tooltip;
37
import javafx.scene.control.TreeItem.TreeModificationEvent;
38
import javafx.scene.input.KeyEvent;
39
import javafx.stage.Stage;
40
import javafx.stage.Window;
41
42
import java.io.File;
43
import java.nio.file.Path;
44
import java.util.*;
45
import java.util.concurrent.atomic.AtomicBoolean;
46
import java.util.function.Function;
47
import java.util.stream.Collectors;
48
49
import static com.keenwrite.Constants.*;
50
import static com.keenwrite.ExportFormat.NONE;
51
import static com.keenwrite.Messages.get;
52
import static com.keenwrite.StatusNotifier.clue;
53
import static com.keenwrite.io.MediaType.*;
54
import static com.keenwrite.preferences.Workspace.*;
55
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
56
import static com.keenwrite.service.events.Notifier.NO;
57
import static com.keenwrite.service.events.Notifier.YES;
58
import static java.util.stream.Collectors.groupingBy;
59
import static javafx.application.Platform.runLater;
60
import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
61
import static javafx.scene.input.KeyCode.SPACE;
62
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
63
import static javafx.util.Duration.millis;
64
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
65
66
/**
67
 * Responsible for wiring together the main application components for a
68
 * particular workspace (project). These include the definition views,
69
 * text editors, and preview pane along with any corresponding controllers.
70
 */
71
public final class MainPane extends SplitPane {
72
  private static final Notifier sNotifier = Services.load( Notifier.class );
73
74
  /**
75
   * Used when opening files to determine how each file should be binned and
76
   * therefore what tab pane to be opened within.
77
   */
78
  private static final Set<MediaType> PLAIN_TEXT_FORMAT = Set.of(
79
    TEXT_MARKDOWN, TEXT_R_MARKDOWN, TEXT_R_XML, UNDEFINED
80
  );
81
82
  /**
83
   * Prevents re-instantiation of processing classes.
84
   */
85
  private final Map<TextResource, Processor<String>> mProcessors =
86
    new HashMap<>();
87
88
  private final Workspace mWorkspace;
89
90
  /**
91
   * Groups similar file type tabs together.
92
   */
93
  private final Map<MediaType, DetachableTabPane> mTabPanes = new HashMap<>();
94
95
  /**
96
   * Stores definition names and values.
97
   */
98
  private final Map<String, String> mResolvedMap =
99
    new HashMap<>( MAP_SIZE_DEFAULT );
100
101
  /**
102
   * Renders the actively selected plain text editor tab.
103
   */
104
  private final HtmlPreview mHtmlPreview;
105
106
  /**
107
   * Changing the active editor fires the value changed event. This allows
108
   * refreshes to happen when external definitions are modified and need to
109
   * trigger the processing chain.
110
   */
111
  private final ObjectProperty<TextEditor> mActiveTextEditor =
112
    createActiveTextEditor();
113
114
  /**
115
   * Changing the active definition editor fires the value changed event. This
116
   * allows refreshes to happen when external definitions are modified and need
117
   * to trigger the processing chain.
118
   */
119
  private final ObjectProperty<TextDefinition> mActiveDefinitionEditor =
120
    createActiveDefinitionEditor( mActiveTextEditor );
121
122
  /**
123
   * Responsible for creating a new scene when a tab is detached into
124
   * its own window frame.
125
   */
126
  private final DefinitionTabSceneFactory mDefinitionTabSceneFactory =
127
    createDefinitionTabSceneFactory( mActiveDefinitionEditor );
128
129
  /**
130
   * Tracks the number of detached tab panels opened into their own windows,
131
   * which allows unique identification of subordinate windows by their title.
132
   * It is doubtful more than 128 windows, much less 256, will be created.
133
   */
134
  private byte mWindowCount;
135
136
  /**
137
   * Called when the definition data is changed.
138
   */
139
  private final EventHandler<TreeModificationEvent<Event>> mTreeHandler =
140
    event -> {
141
      final var editor = mActiveDefinitionEditor.get();
142
143
      resolve( editor );
144
      process( getActiveTextEditor() );
145
      save( editor );
146
    };
147
148
  /**
149
   * Adds all content panels to the main user interface. This will load the
150
   * configuration settings from the workspace to reproduce the settings from
151
   * a previous session.
152
   */
153
  public MainPane( final Workspace workspace ) {
154
    mWorkspace = workspace;
155
    mHtmlPreview = new HtmlPreview( workspace );
156
157
    open( bin( getRecentFiles() ) );
158
    viewPreview();
159
    setDividerPositions( calculateDividerPositions() );
160
161
    // Once the main scene's window regains focus, update the active definition
162
    // editor to the currently selected tab.
163
    runLater(
164
      () -> getWindow().focusedProperty().addListener( ( c, o, n ) -> {
165
        if( n != null && n ) {
166
          final var pane = mTabPanes.get( TEXT_YAML );
167
          final var model = pane.getSelectionModel();
168
          final var tab = model.getSelectedItem();
169
170
          if( tab != null ) {
171
            final var resource = tab.getContent();
172
173
            if( resource instanceof TextDefinition ) {
174
              mActiveDefinitionEditor.set( (TextDefinition) tab.getContent() );
175
            }
176
          }
177
        }
178
      } )
179
    );
180
  }
181
182
  /**
183
   * TODO: Load divider positions from exported settings, see bin() comment.
184
   */
185
  private double[] calculateDividerPositions() {
186
    final var ratio = 100f / getItems().size() / 100;
187
    final var positions = getDividerPositions();
188
189
    for( int i = 0; i < positions.length; i++ ) {
190
      positions[ i ] = ratio * i;
191
    }
192
193
    return positions;
194
  }
195
196
  /**
197
   * Opens all the files into the application, provided the paths are unique.
198
   * This may only be called for any type of files that a user can edit
199
   * (i.e., update and persist), such as definitions and text files.
200
   *
201
   * @param files The list of files to open.
202
   */
203
  public void open( final List<File> files ) {
204
    files.forEach( this::open );
205
  }
206
207
  /**
208
   * This opens the given file. Since the preview pane is not a file that
209
   * can be opened, it is safe to add a listener to the detachable pane.
210
   *
211
   * @param file The file to open.
212
   */
213
  private void open( final File file ) {
214
    final var tab = createTab( file );
215
    final var node = tab.getContent();
216
    final var mediaType = MediaType.valueFrom( file );
217
    final var tabPane = obtainDetachableTabPane( mediaType );
218
    final var newTabPane = !getItems().contains( tabPane );
219
220
    tab.setTooltip( createTooltip( file ) );
221
    tabPane.setFocusTraversable( false );
222
    tabPane.setTabClosingPolicy( ALL_TABS );
223
    tabPane.getTabs().add( tab );
224
225
    if( newTabPane ) {
226
      var index = getItems().size();
227
228
      if( node instanceof TextDefinition ) {
229
        tabPane.setSceneFactory( mDefinitionTabSceneFactory::create );
230
        index = 0;
231
      }
232
233
      addTabPane( index, tabPane );
234
    }
235
236
    getRecentFiles().add( file.getAbsolutePath() );
237
  }
238
239
  /**
240
   * Opens a new text editor document using the default document file name.
241
   */
242
  public void newTextEditor() {
243
    open( DOCUMENT_DEFAULT );
244
  }
245
246
  /**
247
   * Opens a new definition editor document using the default definition
248
   * file name.
249
   */
250
  public void newDefinitionEditor() {
251
    open( DEFINITION_DEFAULT );
252
  }
253
254
  /**
255
   * Iterates over all tab panes to find all {@link TextEditor}s and request
256
   * that they save themselves.
257
   */
258
  public void saveAll() {
259
    mTabPanes.forEach(
260
      ( mt, tp ) -> tp.getTabs().forEach( ( tab ) -> {
261
        final var node = tab.getContent();
262
        if( node instanceof TextEditor ) {
263
          save( ((TextEditor) node) );
264
        }
265
      } )
266
    );
267
  }
268
269
  /**
270
   * Requests that the active {@link TextEditor} saves itself. Don't bother
271
   * checking if modified first because if the user swaps external media from
272
   * an external source (e.g., USB thumb drive), save should not second-guess
273
   * the user: save always re-saves. Also, it's less code.
274
   */
275
  public void save() {
276
    save( getActiveTextEditor() );
277
  }
278
279
  /**
280
   * Saves the active {@link TextEditor} under a new name.
281
   *
282
   * @param file The new active editor {@link File} reference.
283
   */
284
  public void saveAs( final File file ) {
285
    assert file != null;
286
    final var editor = getActiveTextEditor();
287
    final var tab = getTab( editor );
288
289
    editor.rename( file );
290
    tab.ifPresent( t -> {
291
      t.setText( editor.getFilename() );
292
      t.setTooltip( createTooltip( file ) );
293
    } );
294
295
    save();
296
  }
297
298
  /**
299
   * Saves the given {@link TextResource} to a file. This is typically used
300
   * to save either an instance of {@link TextEditor} or {@link TextDefinition}.
301
   *
302
   * @param resource The resource to export.
303
   */
304
  private void save( final TextResource resource ) {
305
    try {
306
      resource.save();
307
    } catch( final Exception ex ) {
308
      clue( ex );
309
      sNotifier.alert(
310
        getWindow(), resource.getPath(), "TextResource.saveFailed", ex
311
      );
312
    }
313
  }
314
315
  /**
316
   * Closes all open {@link TextEditor}s; all {@link TextDefinition}s stay open.
317
   *
318
   * @return {@code true} when all editors, modified or otherwise, were
319
   * permitted to close; {@code false} when one or more editors were modified
320
   * and the user requested no closing.
321
   */
322
  public boolean closeAll() {
323
    var closable = true;
324
325
    for( final var entry : mTabPanes.entrySet() ) {
326
      final var tabPane = entry.getValue();
327
      final var tabIterator = tabPane.getTabs().iterator();
328
329
      while( tabIterator.hasNext() ) {
330
        final var tab = tabIterator.next();
331
        final var node = tab.getContent();
332
333
        if( node instanceof TextEditor &&
334
          (closable &= canClose( (TextEditor) node )) ) {
335
          tabIterator.remove();
336
          close( tab );
337
        }
338
      }
339
    }
340
341
    return closable;
342
  }
343
344
  /**
345
   * Calls the tab's {@link Tab#getOnClosed()} handler to carry out a close
346
   * event.
347
   *
348
   * @param tab The {@link Tab} that was closed.
349
   */
350
  private void close( final Tab tab ) {
351
    final var handler = tab.getOnClosed();
352
353
    if( handler != null ) {
354
      handler.handle( new ActionEvent() );
355
    }
356
  }
357
358
  /**
359
   * Closes the active tab; delegates to {@link #canClose(TextEditor)}.
360
   */
361
  public void close() {
362
    final var editor = getActiveTextEditor();
363
    if( canClose( editor ) ) {
364
      close( editor );
365
    }
366
  }
367
368
  /**
369
   * Closes the given {@link TextEditor}. This must not be called from within
370
   * a loop that iterates over the tab panes using {@code forEach}, lest a
371
   * concurrent modification exception be thrown.
372
   *
373
   * @param editor The {@link TextEditor} to close, without confirming with
374
   *               the user.
375
   */
376
  private void close( final TextEditor editor ) {
377
    getTab( editor ).ifPresent(
378
      ( tab ) -> {
379
        tab.getTabPane().getTabs().remove( tab );
380
        close( tab );
381
      }
382
    );
383
  }
384
385
  /**
386
   * Answers whether the given {@link TextEditor} may be closed.
387
   *
388
   * @param editor The {@link TextEditor} to try closing.
389
   * @return {@code true} when the editor may be closed; {@code false} when
390
   * the user has requested to keep the editor open.
391
   */
392
  private boolean canClose( final TextEditor editor ) {
393
    final var editorTab = getTab( editor );
394
    final var canClose = new AtomicBoolean( true );
395
396
    if( editor.isModified() ) {
397
      final var filename = new StringBuilder();
398
      editorTab.ifPresent( ( tab ) -> filename.append( tab.getText() ) );
399
400
      final var message = sNotifier.createNotification(
401
        Messages.get( "Alert.file.close.title" ),
402
        Messages.get( "Alert.file.close.text" ),
403
        filename.toString()
404
      );
405
406
      final var dialog = sNotifier.createConfirmation( getWindow(), message );
407
408
      dialog.showAndWait().ifPresent(
409
        save -> canClose.set( save == YES ? editor.save() : save == NO )
410
      );
411
    }
412
413
    return canClose.get();
414
  }
415
416
  private ObjectProperty<TextEditor> createActiveTextEditor() {
417
    final var editor = new SimpleObjectProperty<TextEditor>();
418
419
    editor.addListener( ( c, o, n ) -> {
420
      if( n != null ) {
421
        mHtmlPreview.setBaseUri( n.getPath() );
422
        process( n );
423
      }
424
    } );
425
426
    return editor;
427
  }
428
429
  /**
430
   * Adds the HTML preview tab to its own tab pane. This will only add the
431
   * preview once.
432
   */
433
  public void viewPreview() {
434
    final var tabPane = obtainDetachableTabPane( TEXT_HTML );
435
436
    // Prevent multiple HTML previews because in the end, there can be only one.
437
    for( final var tab : tabPane.getTabs() ) {
438
      if( tab.getContent() == mHtmlPreview ) {
439
        return;
440
      }
441
    }
442
443
    tabPane.addTab( "HTML", mHtmlPreview );
444
    addTabPane( tabPane );
445
  }
446
447
  public void viewRefresh() {
448
    mHtmlPreview.refresh();
449
  }
450
451
  /**
452
   * Returns the tab that contains the given {@link TextEditor}.
453
   *
454
   * @param editor The {@link TextEditor} instance to find amongst the tabs.
455
   * @return The first tab having content that matches the given tab.
456
   */
457
  private Optional<Tab> getTab( final TextEditor editor ) {
28
import javafx.application.Platform;
29
import javafx.beans.property.*;
30
import javafx.collections.ListChangeListener;
31
import javafx.event.ActionEvent;
32
import javafx.event.Event;
33
import javafx.event.EventHandler;
34
import javafx.scene.Scene;
35
import javafx.scene.control.SplitPane;
36
import javafx.scene.control.Tab;
37
import javafx.scene.control.Tooltip;
38
import javafx.scene.control.TreeItem.TreeModificationEvent;
39
import javafx.scene.input.KeyEvent;
40
import javafx.stage.Stage;
41
import javafx.stage.Window;
42
43
import java.io.File;
44
import java.nio.file.Path;
45
import java.util.*;
46
import java.util.concurrent.atomic.AtomicBoolean;
47
import java.util.function.Function;
48
import java.util.stream.Collectors;
49
50
import static com.keenwrite.Constants.*;
51
import static com.keenwrite.ExportFormat.NONE;
52
import static com.keenwrite.Messages.get;
53
import static com.keenwrite.StatusNotifier.clue;
54
import static com.keenwrite.io.MediaType.*;
55
import static com.keenwrite.preferences.Workspace.*;
56
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
57
import static com.keenwrite.service.events.Notifier.NO;
58
import static com.keenwrite.service.events.Notifier.YES;
59
import static java.util.stream.Collectors.groupingBy;
60
import static javafx.application.Platform.runLater;
61
import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
62
import static javafx.scene.input.KeyCode.SPACE;
63
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
64
import static javafx.util.Duration.millis;
65
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
66
67
/**
68
 * Responsible for wiring together the main application components for a
69
 * particular workspace (project). These include the definition views,
70
 * text editors, and preview pane along with any corresponding controllers.
71
 */
72
public final class MainPane extends SplitPane {
73
  private static final Notifier sNotifier = Services.load( Notifier.class );
74
75
  /**
76
   * Used when opening files to determine how each file should be binned and
77
   * therefore what tab pane to be opened within.
78
   */
79
  private static final Set<MediaType> PLAIN_TEXT_FORMAT = Set.of(
80
    TEXT_MARKDOWN, TEXT_R_MARKDOWN, TEXT_R_XML, UNDEFINED
81
  );
82
83
  /**
84
   * Prevents re-instantiation of processing classes.
85
   */
86
  private final Map<TextResource, Processor<String>> mProcessors =
87
    new HashMap<>();
88
89
  private final Workspace mWorkspace;
90
91
  /**
92
   * Groups similar file type tabs together.
93
   */
94
  private final Map<MediaType, DetachableTabPane> mTabPanes = new HashMap<>();
95
96
  /**
97
   * Stores definition names and values.
98
   */
99
  private final Map<String, String> mResolvedMap =
100
    new HashMap<>( MAP_SIZE_DEFAULT );
101
102
  /**
103
   * Renders the actively selected plain text editor tab.
104
   */
105
  private final HtmlPreview mHtmlPreview;
106
107
  /**
108
   * Changing the active editor fires the value changed event. This allows
109
   * refreshes to happen when external definitions are modified and need to
110
   * trigger the processing chain.
111
   */
112
  private final ObjectProperty<TextEditor> mActiveTextEditor =
113
    createActiveTextEditor();
114
115
  /**
116
   * Changing the active definition editor fires the value changed event. This
117
   * allows refreshes to happen when external definitions are modified and need
118
   * to trigger the processing chain.
119
   */
120
  private final ObjectProperty<TextDefinition> mActiveDefinitionEditor =
121
    createActiveDefinitionEditor( mActiveTextEditor );
122
123
  /**
124
   * Responsible for creating a new scene when a tab is detached into
125
   * its own window frame.
126
   */
127
  private final DefinitionTabSceneFactory mDefinitionTabSceneFactory =
128
    createDefinitionTabSceneFactory( mActiveDefinitionEditor );
129
130
  /**
131
   * Tracks the number of detached tab panels opened into their own windows,
132
   * which allows unique identification of subordinate windows by their title.
133
   * It is doubtful more than 128 windows, much less 256, will be created.
134
   */
135
  private byte mWindowCount;
136
137
  /**
138
   * Called when the definition data is changed.
139
   */
140
  private final EventHandler<TreeModificationEvent<Event>> mTreeHandler =
141
    event -> {
142
      final var editor = mActiveDefinitionEditor.get();
143
144
      resolve( editor );
145
      process( getActiveTextEditor() );
146
      save( editor );
147
    };
148
149
  /**
150
   * Adds all content panels to the main user interface. This will load the
151
   * configuration settings from the workspace to reproduce the settings from
152
   * a previous session.
153
   */
154
  public MainPane( final Workspace workspace ) {
155
    mWorkspace = workspace;
156
    mHtmlPreview = new HtmlPreview( workspace );
157
158
    open( bin( getRecentFiles() ) );
159
    viewPreview();
160
    setDividerPositions( calculateDividerPositions() );
161
162
    // Once the main scene's window regains focus, update the active definition
163
    // editor to the currently selected tab.
164
    runLater(
165
      () -> {
166
        getWindow().focusedProperty().addListener( ( c, o, n ) -> {
167
          if( n != null && n ) {
168
            final var pane = mTabPanes.get( TEXT_YAML );
169
            final var model = pane.getSelectionModel();
170
            final var tab = model.getSelectedItem();
171
172
            if( tab != null ) {
173
              final var resource = tab.getContent();
174
175
              if( resource instanceof TextDefinition ) {
176
                mActiveDefinitionEditor.set( (TextDefinition) tab.getContent() );
177
              }
178
            }
179
          }
180
        } );
181
182
        getWindow().setOnCloseRequest( ( event ) -> {
183
          // Order matters here. We want to close all the tabs to ensure each
184
          // is saved, but after they are closed, the workspace should still
185
          // retain the list of files that were open. If this line came after
186
          // closing, then restarting the application would list no files.
187
          mWorkspace.save();
188
189
          if( closeAll() ) {
190
            Platform.exit();
191
            System.exit( 0 );
192
          }
193
          else {
194
            event.consume();
195
          }
196
        } );
197
      }
198
    );
199
  }
200
201
  /**
202
   * TODO: Load divider positions from exported settings, see bin() comment.
203
   */
204
  private double[] calculateDividerPositions() {
205
    final var ratio = 100f / getItems().size() / 100;
206
    final var positions = getDividerPositions();
207
208
    for( int i = 0; i < positions.length; i++ ) {
209
      positions[ i ] = ratio * i;
210
    }
211
212
    return positions;
213
  }
214
215
  /**
216
   * Opens all the files into the application, provided the paths are unique.
217
   * This may only be called for any type of files that a user can edit
218
   * (i.e., update and persist), such as definitions and text files.
219
   *
220
   * @param files The list of files to open.
221
   */
222
  public void open( final List<File> files ) {
223
    files.forEach( this::open );
224
  }
225
226
  /**
227
   * This opens the given file. Since the preview pane is not a file that
228
   * can be opened, it is safe to add a listener to the detachable pane.
229
   *
230
   * @param file The file to open.
231
   */
232
  private void open( final File file ) {
233
    final var tab = createTab( file );
234
    final var node = tab.getContent();
235
    final var mediaType = MediaType.valueFrom( file );
236
    final var tabPane = obtainDetachableTabPane( mediaType );
237
    final var newTabPane = !getItems().contains( tabPane );
238
239
    tab.setTooltip( createTooltip( file ) );
240
    tabPane.setFocusTraversable( false );
241
    tabPane.setTabClosingPolicy( ALL_TABS );
242
    tabPane.getTabs().add( tab );
243
244
    if( newTabPane ) {
245
      var index = getItems().size();
246
247
      if( node instanceof TextDefinition ) {
248
        tabPane.setSceneFactory( mDefinitionTabSceneFactory::create );
249
        index = 0;
250
      }
251
252
      addTabPane( index, tabPane );
253
    }
254
255
    getRecentFiles().add( file.getAbsolutePath() );
256
  }
257
258
  /**
259
   * Opens a new text editor document using the default document file name.
260
   */
261
  public void newTextEditor() {
262
    open( DOCUMENT_DEFAULT );
263
  }
264
265
  /**
266
   * Opens a new definition editor document using the default definition
267
   * file name.
268
   */
269
  public void newDefinitionEditor() {
270
    open( DEFINITION_DEFAULT );
271
  }
272
273
  /**
274
   * Iterates over all tab panes to find all {@link TextEditor}s and request
275
   * that they save themselves.
276
   */
277
  public void saveAll() {
278
    mTabPanes.forEach(
279
      ( mt, tp ) -> tp.getTabs().forEach( ( tab ) -> {
280
        final var node = tab.getContent();
281
        if( node instanceof TextEditor ) {
282
          save( ((TextEditor) node) );
283
        }
284
      } )
285
    );
286
  }
287
288
  /**
289
   * Requests that the active {@link TextEditor} saves itself. Don't bother
290
   * checking if modified first because if the user swaps external media from
291
   * an external source (e.g., USB thumb drive), save should not second-guess
292
   * the user: save always re-saves. Also, it's less code.
293
   */
294
  public void save() {
295
    save( getActiveTextEditor() );
296
  }
297
298
  /**
299
   * Saves the active {@link TextEditor} under a new name.
300
   *
301
   * @param file The new active editor {@link File} reference.
302
   */
303
  public void saveAs( final File file ) {
304
    assert file != null;
305
    final var editor = getActiveTextEditor();
306
    final var tab = getTab( editor );
307
308
    editor.rename( file );
309
    tab.ifPresent( t -> {
310
      t.setText( editor.getFilename() );
311
      t.setTooltip( createTooltip( file ) );
312
    } );
313
314
    save();
315
  }
316
317
  /**
318
   * Saves the given {@link TextResource} to a file. This is typically used
319
   * to save either an instance of {@link TextEditor} or {@link TextDefinition}.
320
   *
321
   * @param resource The resource to export.
322
   */
323
  private void save( final TextResource resource ) {
324
    try {
325
      resource.save();
326
    } catch( final Exception ex ) {
327
      clue( ex );
328
      sNotifier.alert(
329
        getWindow(), resource.getPath(), "TextResource.saveFailed", ex
330
      );
331
    }
332
  }
333
334
  /**
335
   * Closes all open {@link TextEditor}s; all {@link TextDefinition}s stay open.
336
   *
337
   * @return {@code true} when all editors, modified or otherwise, were
338
   * permitted to close; {@code false} when one or more editors were modified
339
   * and the user requested no closing.
340
   */
341
  public boolean closeAll() {
342
    var closable = true;
343
344
    for( final var entry : mTabPanes.entrySet() ) {
345
      final var tabPane = entry.getValue();
346
      final var tabIterator = tabPane.getTabs().iterator();
347
348
      while( tabIterator.hasNext() ) {
349
        final var tab = tabIterator.next();
350
        final var resource = tab.getContent();
351
352
        if( !(resource instanceof TextResource) ) {
353
          continue;
354
        }
355
356
        if( canClose( (TextResource) resource ) ) {
357
          tabIterator.remove();
358
          close( tab );
359
        }
360
        else {
361
          closable = false;
362
        }
363
      }
364
    }
365
366
    return closable;
367
  }
368
369
  /**
370
   * Calls the tab's {@link Tab#getOnClosed()} handler to carry out a close
371
   * event.
372
   *
373
   * @param tab The {@link Tab} that was closed.
374
   */
375
  private void close( final Tab tab ) {
376
    final var handler = tab.getOnClosed();
377
378
    if( handler != null ) {
379
      handler.handle( new ActionEvent() );
380
    }
381
  }
382
383
  /**
384
   * Closes the active tab; delegates to {@link #canClose(TextResource)}.
385
   */
386
  public void close() {
387
    final var editor = getActiveTextEditor();
388
    if( canClose( editor ) ) {
389
      close( editor );
390
    }
391
  }
392
393
  /**
394
   * Closes the given {@link TextResource}. This must not be called from within
395
   * a loop that iterates over the tab panes using {@code forEach}, lest a
396
   * concurrent modification exception be thrown.
397
   *
398
   * @param resource The {@link TextResource} to close, without confirming with
399
   *                 the user.
400
   */
401
  private void close( final TextResource resource ) {
402
    getTab( resource ).ifPresent(
403
      ( tab ) -> {
404
        tab.getTabPane().getTabs().remove( tab );
405
        close( tab );
406
      }
407
    );
408
  }
409
410
  /**
411
   * Answers whether the given {@link TextResource} may be closed.
412
   *
413
   * @param editor The {@link TextResource} to try closing.
414
   * @return {@code true} when the editor may be closed; {@code false} when
415
   * the user has requested to keep the editor open.
416
   */
417
  private boolean canClose( final TextResource editor ) {
418
    final var editorTab = getTab( editor );
419
    final var canClose = new AtomicBoolean( true );
420
421
    if( editor.isModified() ) {
422
      final var filename = new StringBuilder();
423
      editorTab.ifPresent( ( tab ) -> filename.append( tab.getText() ) );
424
425
      final var message = sNotifier.createNotification(
426
        Messages.get( "Alert.file.close.title" ),
427
        Messages.get( "Alert.file.close.text" ),
428
        filename.toString()
429
      );
430
431
      final var dialog = sNotifier.createConfirmation( getWindow(), message );
432
433
      dialog.showAndWait().ifPresent(
434
        save -> canClose.set( save == YES ? editor.save() : save == NO )
435
      );
436
    }
437
438
    return canClose.get();
439
  }
440
441
  private ObjectProperty<TextEditor> createActiveTextEditor() {
442
    final var editor = new SimpleObjectProperty<TextEditor>();
443
444
    editor.addListener( ( c, o, n ) -> {
445
      if( n != null ) {
446
        mHtmlPreview.setBaseUri( n.getPath() );
447
        process( n );
448
      }
449
    } );
450
451
    return editor;
452
  }
453
454
  /**
455
   * Adds the HTML preview tab to its own tab pane. This will only add the
456
   * preview once.
457
   */
458
  public void viewPreview() {
459
    final var tabPane = obtainDetachableTabPane( TEXT_HTML );
460
461
    // Prevent multiple HTML previews because in the end, there can be only one.
462
    for( final var tab : tabPane.getTabs() ) {
463
      if( tab.getContent() == mHtmlPreview ) {
464
        return;
465
      }
466
    }
467
468
    tabPane.addTab( "HTML", mHtmlPreview );
469
    addTabPane( tabPane );
470
  }
471
472
  public void viewRefresh() {
473
    mHtmlPreview.refresh();
474
  }
475
476
  /**
477
   * Returns the tab that contains the given {@link TextEditor}.
478
   *
479
   * @param editor The {@link TextEditor} instance to find amongst the tabs.
480
   * @return The first tab having content that matches the given tab.
481
   */
482
  private Optional<Tab> getTab( final TextResource editor ) {
458483
    return mTabPanes.values()
459484
                    .stream()
M src/main/java/com/keenwrite/editors/definition/DefinitionEditor.java
129129
    setCenter( mTreeView );
130130
    setAlignment( buttonBar, TOP_CENTER );
131
    addTreeChangeHandler( event -> mModified.set( true ) );
132131
    mEncoding = open( mFile );
132
133
    // After the file is opened, watch for changes, not before. Otherwise,
134
    // upon saving, users will be prompted to save a file that hasn't had
135
    // any modifications (from their perspective).
136
    addTreeChangeHandler( event -> mModified.set( true ) );
133137
  }
134138
M src/main/java/com/keenwrite/editors/markdown/MarkdownEditor.java
339339
  @Override
340340
  public void fencedCodeBlock() {
341
    final var key = "App.action.insert.fenced_code_block.prompt.text";
342
343
    // TODO: Introduce sample text if nothing is selected.
344
    //enwrap( "\n\n```\n", "\n```\n\n", get( key ) );
341
    enwrap( "\n\n```\n", "\n```\n\n" );
345342
  }
346343
M src/main/java/com/keenwrite/processors/markdown/MarkdownProcessor.java
5151
   *                   extensions used by the Markdown parser.
5252
   */
53
  @Override
5354
  void init(
5455
    final List<Extension> extensions, final ProcessorContext context ) {
5556
    final var editorFile = context.getDocumentPath();
5657
    final var mediaType = MediaType.valueFrom( editorFile );
5758
    final Processor<String> processor;
5859
5960
    if( mediaType == TEXT_R_MARKDOWN || mediaType == TEXT_R_XML ) {
6061
      final var rProcessor = new RProcessor( context );
61
      extensions.add( RExtension.create( rProcessor ) );
62
      extensions.add( RExtension.create( rProcessor, context ) );
6263
      processor = rProcessor;
6364
    }
M src/main/java/com/keenwrite/processors/markdown/extensions/caret/CaretExtension.java
66
import com.keenwrite.processors.ProcessorContext;
77
import com.keenwrite.processors.markdown.extensions.HtmlRendererAdapter;
8
import com.vladsch.flexmark.ext.tables.TableBlock;
89
import com.vladsch.flexmark.html.AttributeProvider;
910
import com.vladsch.flexmark.html.AttributeProviderFactory;
...
1718
1819
import static com.keenwrite.Constants.CARET_ID;
20
import static com.keenwrite.processors.markdown.extensions.r.EmptyNode.EMPTY_NODE;
1921
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
2022
...
4850
  public static class IdAttributeProvider implements AttributeProvider {
4951
    private final Caret mCaret;
52
    private boolean mAdded;
5053
5154
    public IdAttributeProvider( final Caret caret ) {
5255
      mCaret = caret;
5356
    }
5457
55
    private static AttributeProviderFactory createFactory(
56
      final Caret caret ) {
58
    private static AttributeProviderFactory createFactory( final Caret caret ) {
5759
      return new IndependentAttributeProviderFactory() {
5860
        @Override
...
6870
                               @NotNull AttributablePart part,
6971
                               @NotNull MutableAttributes attributes ) {
72
      // Optimization: if a caret is inserted, don't try to find another.
73
      if( mAdded ) {
74
        return;
75
      }
76
77
      // If a table block has been earmarked with an empty node, it means
78
      // another extension has generated code from an external source. The
79
      // Markdown processor won't be able to determine the caret position
80
      // with any semblance of accuracy, so skip the element. This usually
81
      // happens with tables, but in theory any Markdown generated from an
82
      // external source (e.g., an R script) could produce text that has no
83
      // caret position that can be calculated.
84
      var table = curr;
85
86
      if( !(curr instanceof TableBlock) ) {
87
        table = curr.getAncestorOfType( TableBlock.class );
88
      }
89
90
      // The table was generated outside the document
91
      if( table != null && table.getLastChild() == EMPTY_NODE ) {
92
        return;
93
      }
94
7095
      final var outside = mCaret.isAfterText() ? 1 : 0;
7196
      final var began = curr.getStartOffset();
...
81106
        // This line empowers synchronizing the text editor with the preview.
82107
        attributes.addValue( AttributeImpl.of( "id", CARET_ID ) );
108
109
        // We're done until the user moves the caret (micro-optimization)
110
        mAdded = true;
83111
      }
84112
    }
A src/main/java/com/keenwrite/processors/markdown/extensions/r/EmptyNode.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.r;
3
4
import com.vladsch.flexmark.util.ast.Node;
5
import com.vladsch.flexmark.util.sequence.BasedSequence;
6
import org.jetbrains.annotations.NotNull;
7
8
/**
9
 * The singleton is injected into the abstract syntax tree to mark an instance
10
 * of {@link Node} such that it must not be processed normally. Using a wrapper
11
 * for a given {@link Node} cannot work because the class type is used by
12
 * the parsing library for processing.
13
 */
14
public final class EmptyNode extends Node {
15
  public static final Node EMPTY_NODE = new EmptyNode();
16
17
  private static final BasedSequence[] BASE_SEQ = new BasedSequence[ 0 ];
18
19
  private EmptyNode() {
20
  }
21
22
  @Override
23
  public @NotNull BasedSequence[] getSegments() {
24
    return BASE_SEQ;
25
  }
26
}
127
M src/main/java/com/keenwrite/processors/markdown/extensions/r/RExtension.java
33
44
import com.keenwrite.processors.Processor;
5
import com.keenwrite.processors.ProcessorContext;
6
import com.keenwrite.processors.markdown.BaseMarkdownProcessor;
57
import com.keenwrite.processors.r.InlineRProcessor;
68
import com.keenwrite.processors.r.RProcessor;
79
import com.keenwrite.sigils.RSigilOperator;
10
import com.vladsch.flexmark.ast.Paragraph;
811
import com.vladsch.flexmark.ast.Text;
912
import com.vladsch.flexmark.parser.InlineParserExtensionFactory;
...
1922
import java.util.Map;
2023
24
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
25
import static com.keenwrite.processors.markdown.extensions.r.EmptyNode.EMPTY_NODE;
2126
import static com.vladsch.flexmark.parser.Parser.Builder;
2227
import static com.vladsch.flexmark.parser.Parser.ParserExtension;
...
3338
  private final InlineParserFactory FACTORY = CustomParser::new;
3439
  private final RProcessor mProcessor;
40
  private final BaseMarkdownProcessor mMarkdownProcessor;
3541
36
  private RExtension( final RProcessor processor ) {
42
  private RExtension(
43
    final RProcessor processor, final ProcessorContext context ) {
3744
    mProcessor = processor;
45
    mMarkdownProcessor = new BaseMarkdownProcessor( IDENTITY, context );
3846
  }
3947
4048
  /**
4149
   * Creates an extension capable of intercepting R code blocks and preventing
4250
   * them from being converted into HTML {@code <code>} elements.
4351
   */
44
  public static RExtension create( final RProcessor processor ) {
45
    return new RExtension( processor );
52
  public static RExtension create(
53
    final RProcessor processor, final ProcessorContext context ) {
54
    return new RExtension( processor, context );
4655
  }
4756
...
107116
          if( code.startsWith( RSigilOperator.PREFIX ) ) {
108117
            codeNode.unlink();
109
            blockNode.appendChild( new Text( mProcessor.apply( code ) ) );
118
            final var rText = mProcessor.apply( code );
119
            var node = mMarkdownProcessor.toNode( rText );
120
121
            if( node.getFirstChild() instanceof Paragraph ) {
122
              node = new Text( rText );
123
            }
124
            else {
125
              node = node.getFirstChild();
126
127
              if( node != null ) {
128
                // Mark the node as being generated code, such as text returned
129
                // from an R function.
130
                node.appendChild( EMPTY_NODE );
131
              }
132
            }
133
134
            blockNode.appendChild( node );
110135
          }
111136
        }
M src/main/java/com/keenwrite/processors/r/RProcessor.java
1414
  private final Processor<String> mProcessor;
1515
  private final InlineRProcessor mInlineRProcessor;
16
  private volatile boolean mReady;
16
17
  private boolean mReady;
1718
1819
  public RProcessor( final ProcessorContext context ) {
...
2526
  public void init() {
2627
    mReady = mInlineRProcessor.init();
27
  }
28
29
  public boolean isReady() {
30
    return mReady;
3128
  }
3229
3330
  public String apply( final String text ) {
3431
    return mProcessor.apply( text );
32
  }
33
34
  public boolean isReady() {
35
    return mReady;
3536
  }
3637
}
M src/main/java/com/keenwrite/sigils/RSigilOperator.java
88
 */
99
public final class RSigilOperator extends SigilOperator {
10
  public static final char KEY_SEPARATOR_R = '$';
10
  private static final char KEY_SEPARATOR_R = '$';
1111
1212
  public static final String PREFIX = "`r#";
...
2121
  public RSigilOperator( final Tokens tokens, final SigilOperator antecedent ) {
2222
    super( tokens );
23
2324
    mAntecedent = antecedent;
2425
  }
...
4041
   * Transforms a definition key (bracketed by token delimiters) into the
4142
   * expected format for an R variable key name.
43
   * <p>
44
   * The algorithm to entoken a definition name is faster than
45
   * {@link String#replace(char, char)}. Faster still would be to cache the
46
   * values, but that would mean managing the cache when the user changes
47
   * the beginning and ending of the R delimiters. This code gives about a
48
   * 2% performance boost when scrolling using cursor keys. After the JIT
49
   * warms up, this super-minor bottleneck vanishes.
50
   * </p>
4251
   *
4352
   * @param key The variable name to transform, can be empty but not null.
4453
   * @return The transformed variable name.
4554
   */
4655
  public String entoken( final String key ) {
47
    return "v$" + mAntecedent.detoken( key )
48
                             .replace( KEY_SEPARATOR_DEF, KEY_SEPARATOR_R );
56
    final var detokened = new StringBuilder( key.length() );
57
    detokened.append( "v$" );
58
    detokened.append( mAntecedent.detoken( key ) );
59
60
    // The 3 is for "v$X" where X cannot be a period.
61
    for( int i = detokened.length() - 1; i >= 3; i-- ) {
62
      if( detokened.charAt( i ) == KEY_SEPARATOR_DEF ) {
63
        detokened.setCharAt( i, KEY_SEPARATOR_R );
64
      }
65
    }
66
67
    return detokened.toString();
4968
  }
5069
}
M src/main/java/com/keenwrite/ui/dialogs/AbstractDialog.java
6262
   */
6363
  protected final void initCloseAction() {
64
    final Window window = getDialogPane().getScene().getWindow();
64
    final var window = getDialogPane().getScene().getWindow();
6565
    window.setOnCloseRequest( event -> window.hide() );
6666
  }
M src/main/java/com/keenwrite/ui/logging/LogView.java
1919
import static com.keenwrite.Constants.NEWLINE;
2020
import static com.keenwrite.Messages.get;
21
import static com.keenwrite.StatusNotifier.clue;
2122
import static java.time.LocalDateTime.now;
2223
import static java.time.format.DateTimeFormatter.ofPattern;
...
7071
  public void clear() {
7172
    mEntries.clear();
73
    clue();
7274
  }
7375
M src/main/resources/com/keenwrite/messages.properties
8787
8888
workspace.images=Images
89
workspace.images.dir=Relative Directory
90
workspace.images.dir.desc=Path prepended to embedded images referenced using local file paths.
89
workspace.images.dir=Absolute Directory
90
workspace.images.dir.desc=Path to search for local file system images.
9191
workspace.images.dir.title=Directory
9292
workspace.images.order=Extensions