Dave Jarvis' Repositories

M README.md
3838
On other platforms, start the application as follows:
3939
40
1. Download the *full version* of the Java Runtime Environment, [JRE 17](https://bell-sw.com/pages/downloads/?version=java-17).
40
1. Download the *full version* of the Java Runtime Environment, [JRE 18](https://bell-sw.com/pages/downloads/#/java-18).
4141
1. Install the JRE.
4242
1. Open a terminal window.
M README.zh-CN.md
3434
### Other
3535
36
Download and install a full version of [OpenJDK 17](https://bell-sw.com/pages/downloads/#/java-17-lts) that includes JavaFX module support, then run:
36
Download and install a full version of [OpenJDK 18](https://bell-sw.com/pages/downloads/#/java-18) that includes JavaFX module support, then run:
3737
3838
``` bash
M libs/keenquotes.jar
Binary file
M src/main/java/com/keenwrite/MainPane.java
296296
  }
297297
298
  @Subscribe
299
  public void handle( final InsertDefinitionEvent<String> event ) {
300
    final var leaf = event.getLeaf();
301
    final var editor = mTextEditor.get();
302
303
    System.out.println( "INJECT: " + leaf.toPath() );
304
305
    mVariableNameInjector.insert( editor, leaf );
306
  }
307
298308
  private void initAutosave( final Workspace workspace ) {
299309
    final var rate = workspace.integerProperty( KEY_EDITOR_AUTOSAVE );
M src/main/java/com/keenwrite/editors/common/VariableNameInjector.java
6464
          }
6565
          else {
66
            final var mediaType = editor.getMediaType();
67
            final var operator = createOperator( mediaType );
68
69
            editor.replaceText( indexes, operator.apply( leaf.toPath() ) );
66
            insert( editor, leaf );
7067
            definitions.expand( leaf );
7168
          }
7269
        }
7370
      }
7471
    } catch( final Exception ex ) {
7572
      clue( STATUS_DEFINITION_BLANK, ex );
7673
    }
74
  }
75
76
  public void insert(
77
    final TextEditor editor,
78
    final DefinitionTreeItem<String> leaf ) {
79
    assert editor != null;
80
    assert leaf != null;
81
82
    final var mediaType = editor.getMediaType();
83
    final var operator = createOperator( mediaType );
84
    final var indexes = editor.getCaretWord();
85
86
    editor.replaceText( indexes, operator.apply( leaf.toPath() ) );
7787
  }
7888
M src/main/java/com/keenwrite/editors/definition/DefinitionEditor.java
44
import com.keenwrite.constants.Constants;
55
import com.keenwrite.editors.TextDefinition;
6
import com.keenwrite.events.TextDefinitionFocusEvent;
7
import com.keenwrite.processors.r.Engine;
8
import com.keenwrite.ui.tree.AltTreeView;
9
import com.keenwrite.ui.tree.TreeItemConverter;
10
import javafx.beans.property.BooleanProperty;
11
import javafx.beans.property.ReadOnlyBooleanProperty;
12
import javafx.beans.property.SimpleBooleanProperty;
13
import javafx.beans.value.ObservableValue;
14
import javafx.collections.ObservableList;
15
import javafx.event.ActionEvent;
16
import javafx.event.Event;
17
import javafx.event.EventHandler;
18
import javafx.scene.Node;
19
import javafx.scene.control.*;
20
import javafx.scene.input.KeyEvent;
21
import javafx.scene.layout.BorderPane;
22
import javafx.scene.layout.HBox;
23
24
import java.io.File;
25
import java.nio.charset.Charset;
26
import java.util.*;
27
28
import static com.keenwrite.Messages.get;
29
import static com.keenwrite.constants.Constants.*;
30
import static com.keenwrite.events.StatusEvent.clue;
31
import static com.keenwrite.ui.fonts.IconFactory.createGraphic;
32
import static javafx.geometry.Pos.CENTER;
33
import static javafx.geometry.Pos.TOP_CENTER;
34
import static javafx.scene.control.SelectionMode.MULTIPLE;
35
import static javafx.scene.control.TreeItem.childrenModificationEvent;
36
import static javafx.scene.control.TreeItem.valueChangedEvent;
37
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
38
39
/**
40
 * Provides the user interface that holds a {@link TreeView}, which
41
 * allows users to interact with key/value pairs loaded from the
42
 * document parser and adapted using a {@link TreeTransformer}.
43
 */
44
public final class DefinitionEditor extends BorderPane
45
  implements TextDefinition {
46
47
  /**
48
   * Contains the root that is added to the view.
49
   */
50
  private final DefinitionTreeItem<String> mTreeRoot = createRootTreeItem();
51
52
  /**
53
   * Contains a view of the definitions.
54
   */
55
  private final TreeView<String> mTreeView =
56
    new AltTreeView<>( mTreeRoot, new TreeItemConverter() );
57
58
  /**
59
   * Used to adapt the structured document into a {@link TreeView}.
60
   */
61
  private final TreeTransformer mTreeTransformer;
62
63
  /**
64
   * Handlers for key press events.
65
   */
66
  private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
67
    = new HashSet<>();
68
69
  private final Map<String, String> mDefinitions = new HashMap<>();
70
71
  /**
72
   * Opened file's character encoding, or {@link Constants#DEFAULT_CHARSET} if
73
   * either no encoding could be determined or this is a new (empty) file.
74
   */
75
  private final Charset mEncoding;
76
77
  /**
78
   * Tracks whether the in-memory definitions have changed with respect to the
79
   * persisted definitions.
80
   */
81
  private final BooleanProperty mModified = new SimpleBooleanProperty();
82
83
  /**
84
   * File being edited by this editor instance, which may be renamed.
85
   */
86
  private File mFile;
87
88
  /**
89
   * This is provided for unit tests that are not backed by files.
90
   *
91
   * @param treeTransformer Responsible for transforming the definitions into
92
   *                        {@link TreeItem} instances.
93
   */
94
  public DefinitionEditor(
95
    final TreeTransformer treeTransformer ) {
96
    this( DEFINITION_DEFAULT, treeTransformer );
97
  }
98
99
  /**
100
   * Constructs a definition pane with a given tree view root.
101
   *
102
   * @param file The file of definitions to maintain through the UI.
103
   */
104
  public DefinitionEditor(
105
    final File file,
106
    final TreeTransformer treeTransformer ) {
107
    assert file != null;
108
    assert treeTransformer != null;
109
110
    mFile = file;
111
    mTreeTransformer = treeTransformer;
112
113
    mTreeView.setContextMenu( createContextMenu() );
114
    mTreeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
115
    mTreeView.focusedProperty().addListener( this::focused );
116
    getSelectionModel().setSelectionMode( MULTIPLE );
117
118
    final var buttonBar = new HBox();
119
    buttonBar.getChildren().addAll(
120
      createButton( "create", e -> createDefinition() ),
121
      createButton( "rename", e -> renameDefinition() ),
122
      createButton( "delete", e -> deleteDefinitions() )
123
    );
124
    buttonBar.setAlignment( CENTER );
125
    buttonBar.setSpacing( UI_CONTROL_SPACING );
126
    setTop( buttonBar );
127
    setCenter( mTreeView );
128
    setAlignment( buttonBar, TOP_CENTER );
129
130
    mEncoding = open( mFile );
131
    updateDefinitions( getDefinitions(), getTreeView().getRoot() );
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 -> {
137
      mModified.set( true );
138
      updateDefinitions( getDefinitions(), getTreeView().getRoot() );
139
    } );
140
  }
141
142
  /**
143
   * Replaces the given list of variable definitions with a flat hierarchy
144
   * of the converted {@link TreeView} root.
145
   *
146
   * @param definitions The definition map to update.
147
   * @param root        The values to flatten then insert into the map.
148
   */
149
  private void updateDefinitions(
150
    final Map<String, String> definitions,
151
    final TreeItem<String> root ) {
152
    definitions.clear();
153
    definitions.putAll( TreeItemMapper.convert( root ) );
154
    Engine.clear();
155
  }
156
157
  /**
158
   * Returns the variable definitions.
159
   *
160
   * @return The definition map.
161
   */
162
  @Override
163
  public Map<String, String> getDefinitions() {
164
    return mDefinitions;
165
  }
166
167
  @Override
168
  public void setText( final String document ) {
169
    final var foster = mTreeTransformer.transform( document );
170
    final var biological = getTreeRoot();
171
172
    for( final var child : foster.getChildren() ) {
173
      biological.getChildren().add( child );
174
    }
175
176
    getTreeView().refresh();
177
  }
178
179
  @Override
180
  public String getText() {
181
    final var result = new StringBuilder( 32768 );
182
183
    try {
184
      final var root = getTreeView().getRoot();
185
      final var problem = isTreeWellFormed();
186
187
      problem.ifPresentOrElse(
188
        node -> clue( "yaml.error.tree.form", node ),
189
        () -> result.append( mTreeTransformer.transform( root ) )
190
      );
191
    } catch( final Exception ex ) {
192
      // Catch errors while checking for a well-formed tree (e.g., stack smash).
193
      // Also catch any transformation exceptions (e.g., Json processing).
194
      clue( ex );
195
    }
196
197
    return result.toString();
198
  }
199
200
  @Override
201
  public File getFile() {
202
    return mFile;
203
  }
204
205
  @Override
206
  public void rename( final File file ) {
207
    mFile = file;
208
  }
209
210
  @Override
211
  public Charset getEncoding() {
212
    return mEncoding;
213
  }
214
215
  @Override
216
  public Node getNode() {
217
    return this;
218
  }
219
220
  @Override
221
  public ReadOnlyBooleanProperty modifiedProperty() {
222
    return mModified;
223
  }
224
225
  @Override
226
  public void clearModifiedProperty() {
227
    mModified.setValue( false );
228
  }
229
230
  private Button createButton(
231
    final String msgKey, final EventHandler<ActionEvent> eventHandler ) {
232
    final var keyPrefix = Constants.ACTION_PREFIX + "definition." + msgKey;
233
    final var button = new Button( get( keyPrefix + ".text" ) );
234
    final var graphic = createGraphic( get( keyPrefix + ".icon" ) );
235
236
    button.setOnAction( eventHandler );
237
    button.setGraphic( graphic );
238
    button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) );
239
240
    return button;
241
  }
242
243
  /**
244
   * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
245
   * is modified. The modifications include: item value changes, item additions,
246
   * and item removals.
247
   * <p>
248
   * Safe to call multiple times; if a handler is already registered, the
249
   * old handler is used.
250
   * </p>
251
   *
252
   * @param handler The handler to call whenever any {@link TreeItem} changes.
253
   */
254
  public void addTreeChangeHandler(
255
    final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
256
    final var root = getTreeView().getRoot();
257
    root.addEventHandler( valueChangedEvent(), handler );
258
    root.addEventHandler( childrenModificationEvent(), handler );
259
  }
260
261
  /**
262
   * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
263
   * well-formed for export. A tree is considered well-formed if the following
264
   * conditions are met:
265
   *
266
   * <ul>
267
   *   <li>The root node contains at least one child node having a leaf.</li>
268
   *   <li>There are no leaf nodes with sibling leaf nodes.</li>
269
   * </ul>
270
   *
271
   * @return {@code null} if the document is well-formed, otherwise the
272
   * problematic child {@link TreeItem}.
273
   */
274
  public Optional<TreeItem<String>> isTreeWellFormed() {
275
    final var root = getTreeView().getRoot();
276
277
    for( final var child : root.getChildren() ) {
278
      final var problemChild = isWellFormed( child );
279
280
      if( child.isLeaf() || problemChild != null ) {
281
        return Optional.ofNullable( problemChild );
282
      }
283
    }
284
285
    return Optional.empty();
286
  }
287
288
  /**
289
   * Determines whether the document is well-formed by ensuring that
290
   * child branches do not contain multiple leaves.
291
   *
292
   * @param item The sub-tree to check for well-formedness.
293
   * @return {@code null} when the tree is well-formed, otherwise the
294
   * problematic {@link TreeItem}.
295
   */
296
  private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
297
    int childLeafs = 0;
298
    int childBranches = 0;
299
300
    for( final var child : item.getChildren() ) {
301
      if( child.isLeaf() ) {
302
        childLeafs++;
303
      }
304
      else {
305
        childBranches++;
306
      }
307
308
      final var problemChild = isWellFormed( child );
309
310
      if( problemChild != null ) {
311
        return problemChild;
312
      }
313
    }
314
315
    return ((childBranches > 0 && childLeafs == 0) ||
316
      (childBranches == 0 && childLeafs <= 1)) ? null : item;
317
  }
318
319
  @Override
320
  public DefinitionTreeItem<String> findLeafExact( final String text ) {
321
    return getTreeRoot().findLeafExact( text );
322
  }
323
324
  @Override
325
  public DefinitionTreeItem<String> findLeafContains( final String text ) {
326
    return getTreeRoot().findLeafContains( text );
327
  }
328
329
  @Override
330
  public DefinitionTreeItem<String> findLeafContainsNoCase(
331
    final String text ) {
332
    return getTreeRoot().findLeafContainsNoCase( text );
333
  }
334
335
  @Override
336
  public DefinitionTreeItem<String> findLeafStartsWith( final String text ) {
337
    return getTreeRoot().findLeafStartsWith( text );
338
  }
339
340
  public void select( final TreeItem<String> item ) {
341
    getSelectionModel().clearSelection();
342
    getSelectionModel().select( getTreeView().getRow( item ) );
343
  }
344
345
  /**
346
   * Collapses the tree, recursively.
347
   */
348
  public void collapse() {
349
    collapse( getTreeRoot().getChildren() );
350
  }
351
352
  /**
353
   * Collapses the tree, recursively.
354
   *
355
   * @param <T>   The type of tree item to expand (usually String).
356
   * @param nodes The nodes to collapse.
357
   */
358
  private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
359
    for( final var node : nodes ) {
360
      node.setExpanded( false );
361
      collapse( node.getChildren() );
362
    }
363
  }
364
365
  /**
366
   * @return {@code true} when the user is editing a {@link TreeItem}.
367
   */
368
  private boolean isEditingTreeItem() {
369
    return getTreeView().editingItemProperty().getValue() != null;
370
  }
371
372
  /**
373
   * Changes to edit mode for the selected item.
374
   */
375
  @Override
376
  public void renameDefinition() {
377
    getTreeView().edit( getSelectedItem() );
378
  }
379
380
  /**
381
   * Removes all selected items from the {@link TreeView}.
382
   */
383
  @Override
384
  public void deleteDefinitions() {
385
    for( final var item : getSelectedItems() ) {
386
      final var parent = item.getParent();
387
388
      if( parent != null ) {
389
        parent.getChildren().remove( item );
390
      }
391
    }
392
  }
393
394
  /**
395
   * Deletes the selected item.
396
   */
397
  private void deleteSelectedItem() {
398
    final var c = getSelectedItem();
399
    getSiblings( c ).remove( c );
400
  }
401
402
  /**
403
   * Adds a new item under the selected item (or root if nothing is selected).
404
   * There are a few conditions to consider: when adding to the root,
405
   * when adding to a leaf, and when adding to a non-leaf. Items added to the
406
   * root must contain two items: a key and a value.
407
   */
408
  @Override
409
  public void createDefinition() {
410
    final var value = createDefinitionTreeItem();
411
    getSelectedItem().getChildren().add( value );
412
    expand( value );
413
    select( value );
414
  }
415
416
  private ContextMenu createContextMenu() {
417
    final var menu = new ContextMenu();
418
    final var items = menu.getItems();
419
420
    addMenuItem( items, ACTION_PREFIX + "definition.create.text" )
421
      .setOnAction( e -> createDefinition() );
422
    addMenuItem( items, ACTION_PREFIX + "definition.rename.text" )
423
      .setOnAction( e -> renameDefinition() );
424
    addMenuItem( items, ACTION_PREFIX + "definition.delete.text" )
425
      .setOnAction( e -> deleteSelectedItem() );
6
import com.keenwrite.events.InsertDefinitionEvent;
7
import com.keenwrite.events.TextDefinitionFocusEvent;
8
import com.keenwrite.processors.r.Engine;
9
import com.keenwrite.ui.tree.AltTreeView;
10
import com.keenwrite.ui.tree.TreeItemConverter;
11
import javafx.beans.property.BooleanProperty;
12
import javafx.beans.property.ReadOnlyBooleanProperty;
13
import javafx.beans.property.SimpleBooleanProperty;
14
import javafx.beans.value.ObservableValue;
15
import javafx.collections.ObservableList;
16
import javafx.event.ActionEvent;
17
import javafx.event.Event;
18
import javafx.event.EventHandler;
19
import javafx.scene.Node;
20
import javafx.scene.control.*;
21
import javafx.scene.input.KeyEvent;
22
import javafx.scene.layout.BorderPane;
23
import javafx.scene.layout.HBox;
24
25
import java.io.File;
26
import java.nio.charset.Charset;
27
import java.util.*;
28
29
import static com.keenwrite.Messages.get;
30
import static com.keenwrite.constants.Constants.*;
31
import static com.keenwrite.events.StatusEvent.clue;
32
import static com.keenwrite.ui.fonts.IconFactory.createGraphic;
33
import static javafx.geometry.Pos.CENTER;
34
import static javafx.geometry.Pos.TOP_CENTER;
35
import static javafx.scene.control.SelectionMode.MULTIPLE;
36
import static javafx.scene.control.TreeItem.childrenModificationEvent;
37
import static javafx.scene.control.TreeItem.valueChangedEvent;
38
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
39
40
/**
41
 * Provides the user interface that holds a {@link TreeView}, which
42
 * allows users to interact with key/value pairs loaded from the
43
 * document parser and adapted using a {@link TreeTransformer}.
44
 */
45
public final class DefinitionEditor extends BorderPane
46
  implements TextDefinition {
47
48
  /**
49
   * Contains the root that is added to the view.
50
   */
51
  private final DefinitionTreeItem<String> mTreeRoot = createRootTreeItem();
52
53
  /**
54
   * Contains a view of the definitions.
55
   */
56
  private final TreeView<String> mTreeView =
57
    new AltTreeView<>( mTreeRoot, new TreeItemConverter() );
58
59
  /**
60
   * Used to adapt the structured document into a {@link TreeView}.
61
   */
62
  private final TreeTransformer mTreeTransformer;
63
64
  /**
65
   * Handlers for key press events.
66
   */
67
  private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
68
    = new HashSet<>();
69
70
  private final Map<String, String> mDefinitions = new HashMap<>();
71
72
  /**
73
   * Opened file's character encoding, or {@link Constants#DEFAULT_CHARSET} if
74
   * either no encoding could be determined or this is a new (empty) file.
75
   */
76
  private final Charset mEncoding;
77
78
  /**
79
   * Tracks whether the in-memory definitions have changed with respect to the
80
   * persisted definitions.
81
   */
82
  private final BooleanProperty mModified = new SimpleBooleanProperty();
83
84
  /**
85
   * File being edited by this editor instance, which may be renamed.
86
   */
87
  private File mFile;
88
89
  /**
90
   * This is provided for unit tests that are not backed by files.
91
   *
92
   * @param treeTransformer Responsible for transforming the definitions into
93
   *                        {@link TreeItem} instances.
94
   */
95
  public DefinitionEditor(
96
    final TreeTransformer treeTransformer ) {
97
    this( DEFINITION_DEFAULT, treeTransformer );
98
  }
99
100
  /**
101
   * Constructs a definition pane with a given tree view root.
102
   *
103
   * @param file The file of definitions to maintain through the UI.
104
   */
105
  public DefinitionEditor(
106
    final File file,
107
    final TreeTransformer treeTransformer ) {
108
    assert file != null;
109
    assert treeTransformer != null;
110
111
    mFile = file;
112
    mTreeTransformer = treeTransformer;
113
114
    mTreeView.setContextMenu( createContextMenu() );
115
    mTreeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
116
    mTreeView.focusedProperty().addListener( this::focused );
117
    getSelectionModel().setSelectionMode( MULTIPLE );
118
119
    final var buttonBar = new HBox();
120
    buttonBar.getChildren().addAll(
121
      createButton( "create", e -> createDefinition() ),
122
      createButton( "rename", e -> renameDefinition() ),
123
      createButton( "delete", e -> deleteDefinitions() )
124
    );
125
    buttonBar.setAlignment( CENTER );
126
    buttonBar.setSpacing( UI_CONTROL_SPACING );
127
    setTop( buttonBar );
128
    setCenter( mTreeView );
129
    setAlignment( buttonBar, TOP_CENTER );
130
131
    mEncoding = open( mFile );
132
    updateDefinitions( getDefinitions(), getTreeView().getRoot() );
133
134
    // After the file is opened, watch for changes, not before. Otherwise,
135
    // upon saving, users will be prompted to save a file that hasn't had
136
    // any modifications (from their perspective).
137
    addTreeChangeHandler( event -> {
138
      mModified.set( true );
139
      updateDefinitions( getDefinitions(), getTreeView().getRoot() );
140
    } );
141
  }
142
143
  /**
144
   * Replaces the given list of variable definitions with a flat hierarchy
145
   * of the converted {@link TreeView} root.
146
   *
147
   * @param definitions The definition map to update.
148
   * @param root        The values to flatten then insert into the map.
149
   */
150
  private void updateDefinitions(
151
    final Map<String, String> definitions,
152
    final TreeItem<String> root ) {
153
    definitions.clear();
154
    definitions.putAll( TreeItemMapper.convert( root ) );
155
    Engine.clear();
156
  }
157
158
  /**
159
   * Returns the variable definitions.
160
   *
161
   * @return The definition map.
162
   */
163
  @Override
164
  public Map<String, String> getDefinitions() {
165
    return mDefinitions;
166
  }
167
168
  @Override
169
  public void setText( final String document ) {
170
    final var foster = mTreeTransformer.transform( document );
171
    final var biological = getTreeRoot();
172
173
    for( final var child : foster.getChildren() ) {
174
      biological.getChildren().add( child );
175
    }
176
177
    getTreeView().refresh();
178
  }
179
180
  @Override
181
  public String getText() {
182
    final var result = new StringBuilder( 32768 );
183
184
    try {
185
      final var root = getTreeView().getRoot();
186
      final var problem = isTreeWellFormed();
187
188
      problem.ifPresentOrElse(
189
        node -> clue( "yaml.error.tree.form", node ),
190
        () -> result.append( mTreeTransformer.transform( root ) )
191
      );
192
    } catch( final Exception ex ) {
193
      // Catch errors while checking for a well-formed tree (e.g., stack smash).
194
      // Also catch any transformation exceptions (e.g., Json processing).
195
      clue( ex );
196
    }
197
198
    return result.toString();
199
  }
200
201
  @Override
202
  public File getFile() {
203
    return mFile;
204
  }
205
206
  @Override
207
  public void rename( final File file ) {
208
    mFile = file;
209
  }
210
211
  @Override
212
  public Charset getEncoding() {
213
    return mEncoding;
214
  }
215
216
  @Override
217
  public Node getNode() {
218
    return this;
219
  }
220
221
  @Override
222
  public ReadOnlyBooleanProperty modifiedProperty() {
223
    return mModified;
224
  }
225
226
  @Override
227
  public void clearModifiedProperty() {
228
    mModified.setValue( false );
229
  }
230
231
  private Button createButton(
232
    final String msgKey, final EventHandler<ActionEvent> eventHandler ) {
233
    final var keyPrefix = Constants.ACTION_PREFIX + "definition." + msgKey;
234
    final var button = new Button( get( keyPrefix + ".text" ) );
235
    final var graphic = createGraphic( get( keyPrefix + ".icon" ) );
236
237
    button.setOnAction( eventHandler );
238
    button.setGraphic( graphic );
239
    button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) );
240
241
    return button;
242
  }
243
244
  /**
245
   * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
246
   * is modified. The modifications include: item value changes, item additions,
247
   * and item removals.
248
   * <p>
249
   * Safe to call multiple times; if a handler is already registered, the
250
   * old handler is used.
251
   * </p>
252
   *
253
   * @param handler The handler to call whenever any {@link TreeItem} changes.
254
   */
255
  public void addTreeChangeHandler(
256
    final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
257
    final var root = getTreeView().getRoot();
258
    root.addEventHandler( valueChangedEvent(), handler );
259
    root.addEventHandler( childrenModificationEvent(), handler );
260
  }
261
262
  /**
263
   * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
264
   * well-formed for export. A tree is considered well-formed if the following
265
   * conditions are met:
266
   *
267
   * <ul>
268
   *   <li>The root node contains at least one child node having a leaf.</li>
269
   *   <li>There are no leaf nodes with sibling leaf nodes.</li>
270
   * </ul>
271
   *
272
   * @return {@code null} if the document is well-formed, otherwise the
273
   * problematic child {@link TreeItem}.
274
   */
275
  public Optional<TreeItem<String>> isTreeWellFormed() {
276
    final var root = getTreeView().getRoot();
277
278
    for( final var child : root.getChildren() ) {
279
      final var problemChild = isWellFormed( child );
280
281
      if( child.isLeaf() || problemChild != null ) {
282
        return Optional.ofNullable( problemChild );
283
      }
284
    }
285
286
    return Optional.empty();
287
  }
288
289
  /**
290
   * Determines whether the document is well-formed by ensuring that
291
   * child branches do not contain multiple leaves.
292
   *
293
   * @param item The subtree to check for well-formedness.
294
   * @return {@code null} when the tree is well-formed, otherwise the
295
   * problematic {@link TreeItem}.
296
   */
297
  private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
298
    int childLeafs = 0;
299
    int childBranches = 0;
300
301
    for( final var child : item.getChildren() ) {
302
      if( child.isLeaf() ) {
303
        childLeafs++;
304
      }
305
      else {
306
        childBranches++;
307
      }
308
309
      final var problemChild = isWellFormed( child );
310
311
      if( problemChild != null ) {
312
        return problemChild;
313
      }
314
    }
315
316
    return ((childBranches > 0 && childLeafs == 0) ||
317
      (childBranches == 0 && childLeafs <= 1)) ? null : item;
318
  }
319
320
  @Override
321
  public DefinitionTreeItem<String> findLeafExact( final String text ) {
322
    return getTreeRoot().findLeafExact( text );
323
  }
324
325
  @Override
326
  public DefinitionTreeItem<String> findLeafContains( final String text ) {
327
    return getTreeRoot().findLeafContains( text );
328
  }
329
330
  @Override
331
  public DefinitionTreeItem<String> findLeafContainsNoCase(
332
    final String text ) {
333
    return getTreeRoot().findLeafContainsNoCase( text );
334
  }
335
336
  @Override
337
  public DefinitionTreeItem<String> findLeafStartsWith( final String text ) {
338
    return getTreeRoot().findLeafStartsWith( text );
339
  }
340
341
  public void select( final TreeItem<String> item ) {
342
    getSelectionModel().clearSelection();
343
    getSelectionModel().select( getTreeView().getRow( item ) );
344
  }
345
346
  /**
347
   * Collapses the tree, recursively.
348
   */
349
  public void collapse() {
350
    collapse( getTreeRoot().getChildren() );
351
  }
352
353
  /**
354
   * Collapses the tree, recursively.
355
   *
356
   * @param <T>   The type of tree item to expand (usually String).
357
   * @param nodes The nodes to collapse.
358
   */
359
  private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
360
    for( final var node : nodes ) {
361
      node.setExpanded( false );
362
      collapse( node.getChildren() );
363
    }
364
  }
365
366
  /**
367
   * @return {@code true} when the user is editing a {@link TreeItem}.
368
   */
369
  private boolean isEditingTreeItem() {
370
    return getTreeView().editingItemProperty().getValue() != null;
371
  }
372
373
  /**
374
   * Changes to edit mode for the selected item.
375
   */
376
  @Override
377
  public void renameDefinition() {
378
    getTreeView().edit( getSelectedItem() );
379
  }
380
381
  /**
382
   * Removes all selected items from the {@link TreeView}.
383
   */
384
  @Override
385
  public void deleteDefinitions() {
386
    for( final var item : getSelectedItems() ) {
387
      final var parent = item.getParent();
388
389
      if( parent != null ) {
390
        parent.getChildren().remove( item );
391
      }
392
    }
393
  }
394
395
  /**
396
   * Deletes the selected item.
397
   */
398
  private void deleteSelectedItem() {
399
    final var c = getSelectedItem();
400
    getSiblings( c ).remove( c );
401
  }
402
403
  private void insertSelectedItem() {
404
    if( getSelectedItem() instanceof DefinitionTreeItem<String> node ) {
405
      if( node.isLeaf() ) {
406
        InsertDefinitionEvent.fire( node );
407
      }
408
    }
409
  }
410
411
  /**
412
   * Adds a new item under the selected item (or root if nothing is selected).
413
   * There are a few conditions to consider: when adding to the root,
414
   * when adding to a leaf, and when adding to a non-leaf. Items added to the
415
   * root must contain two items: a key and a value.
416
   */
417
  @Override
418
  public void createDefinition() {
419
    final var value = createDefinitionTreeItem();
420
    getSelectedItem().getChildren().add( value );
421
    expand( value );
422
    select( value );
423
  }
424
425
  private ContextMenu createContextMenu() {
426
    final var menu = new ContextMenu();
427
    final var items = menu.getItems();
428
429
    addMenuItem( items, ACTION_PREFIX + "definition.create.text" )
430
      .setOnAction( e -> createDefinition() );
431
    addMenuItem( items, ACTION_PREFIX + "definition.rename.text" )
432
      .setOnAction( e -> renameDefinition() );
433
    addMenuItem( items, ACTION_PREFIX + "definition.delete.text" )
434
      .setOnAction( e -> deleteSelectedItem() );
435
    addMenuItem( items, ACTION_PREFIX + "definition.insert.text" )
436
      .setOnAction( e -> insertSelectedItem() );
426437
427438
    return menu;
A src/main/java/com/keenwrite/events/InsertDefinitionEvent.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
import com.keenwrite.editors.definition.DefinitionTreeItem;
5
6
/**
7
 * Collates information about a request to insert a reference to a
8
 * definition value into the active document.
9
 */
10
public class InsertDefinitionEvent<T> implements AppEvent {
11
12
  private final DefinitionTreeItem<T> mLeaf;
13
14
  private InsertDefinitionEvent( final DefinitionTreeItem<T> leaf ) {
15
    mLeaf = leaf;
16
  }
17
18
  public static <T> void fire( final DefinitionTreeItem<T> leaf ) {
19
    assert leaf != null;
20
    assert leaf.isLeaf();
21
22
    new InsertDefinitionEvent<>( leaf ).publish();
23
  }
24
25
  /**
26
   * Returns the {@link DefinitionTreeItem} that is to be inserted into the
27
   * active document.
28
   *
29
   * @return The item to insert (as a variable).
30
   */
31
  public DefinitionTreeItem<T> getLeaf() {
32
    return mLeaf;
33
  }
34
}
135
M src/main/java/com/keenwrite/processors/XhtmlProcessor.java
44
import com.keenwrite.dom.DocumentParser;
55
import com.keenwrite.ui.heuristics.WordCounter;
6
import com.whitemagicsoftware.keenquotes.Contractions;
7
import com.whitemagicsoftware.keenquotes.Converter;
6
import com.whitemagicsoftware.keenquotes.parser.Contractions;
7
import com.whitemagicsoftware.keenquotes.parser.Curler;
88
import org.w3c.dom.Document;
99
...
2121
import static com.keenwrite.io.HttpFacade.httpGet;
2222
import static com.keenwrite.util.ProtocolScheme.getProtocol;
23
import static com.whitemagicsoftware.keenquotes.Converter.CHARS;
24
import static com.whitemagicsoftware.keenquotes.ParserFactory.ParserType.PARSER_XML;
23
import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML;
24
import static com.whitemagicsoftware.keenquotes.parser.Curler.CHARS;
2525
import static java.lang.String.format;
2626
import static java.lang.String.valueOf;
...
3434
 */
3535
public final class XhtmlProcessor extends ExecutorProcessor<String> {
36
  private final static Converter sTypographer = new Converter(
37
    lex -> clue( lex.toString() ), contractions(), CHARS, PARSER_XML );
36
  private final static Curler sTypographer =
37
    new Curler( contractions(), CHARS, FILTER_XML );
3838
3939
  private final ProcessorContext mContext;
...
280280
   */
281281
  private static Contractions contractions() {
282
    final var builder = new Contractions.Builder();
283
    return builder.withBeganUnambiguous( List.of( "bout" ) ).build();
282
    return new Contractions.Builder().build();
284283
  }
285284
}
M src/main/java/com/keenwrite/typesetting/Typesetter.java
6969
7070
    /**
71
     * @see #setThemePath(Path)
72
     */
73
    public void setThemePath( final File themePath ) {
74
      setThemePath( themePath.toPath() );
75
    }
76
77
    /**
7871
     * @param autoClean {@code true} to remove all temporary files after
7972
     *                  typesetter produces a PDF file.