Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Eliminate Utils class, remove duplicate variables reference

AuthorDaveJarvis <email>
Date2020-11-14 19:52:24 GMT-0800
Commitde49422e1225facdb148a12ce8575f6c4b161e13
Parent1435ae5
Delta741 lines added, 1188 lines removed, 447-line decrease
src/main/resources/com/keenwrite/scene.css
*/
-/*---- toolbar ----*/
-
.tool-bar {
-fx-spacing: 0;
src/main/resources/com/keenwrite/settings.properties
# File and Path References
# ########################################################################
-file.stylesheet.dock=com/keenwrite/dock/dock.css
+#file.stylesheet.dock=com/panemu/tiwulfx/control/dock/tiwulfx-dock.css
file.stylesheet.markdown=${application.package}/editor/markdown.css
file.stylesheet.preview=webview.css
src/main/resources/com/keenwrite/variables.yaml
----
-c:
- protagonist:
- name:
- First: Chloe
- First_pos: $c.protagonist.name.First$'s
- Middle: Irene
- Family: Angelos
- nick:
- Father: Savant
- Mother: Sweetie
- colour:
- eyes: green
- hair: dark auburn
- syn_1: black
- syn_2: purple
- syn_11: teal
- syn_6: silver
- favourite: emerald green
- speech:
- tic: oh
- father:
- heritage: Greek
- name:
- Short: Bryce
- First: Bryson
- First_pos: $c.protagonist.father.name.First$'s
- Honourific: Mr.
- education: Masters
- vocation:
- name: robotics
- title: roboticist
- employer:
- name:
- Short: Rabota
- Full: $c.protagonist.father.employer.name.Short$ Designs
- hair:
- style: thick, curly
- colour: black
- eyes:
- colour: dark brown
- Endear: Dad
- vehicle: coupé
- mother:
- name:
- Short: Cass
- First: Cassandra
- First_pos: $c.protagonist.mother.name.First$'s
- Honourific: Mrs.
- education: PhD
- speech:
- tic: cute
- Honorific: Doctor
- vocation:
- article: an
- name: oceanography
- title: oceanographer
- employer:
- name:
- Full: Oregon State University
- Short: OSU
- eyes:
- colour: blue
- hair:
- style: thick, curly
- colour: dark brown
- Endear: Mom
- Endear_pos: Mom's
- uncle:
- name:
- First: Damian
- First_pos: $c.protagonist.uncle.name.First$'s
- Family: Moros
- hands:
- fingers:
- shape: long, bony
- friend:
- primary:
- name:
- First: Gerard
- First_pos: $c.protagonist.friend.primary.name.First$'s
- Family: Baran
- Family_pos: $c.protagonist.friend.primary.name.Family$'s
- favourite:
- colour: midnight blue
- eyes:
- colour: hazel
- mother:
- name:
- First: Isabella
- Short: Izzy
- Honourific: Mrs.
- father:
- name:
- Short: Mo
- First: Montgomery
- First_pos: $c.protagonist.friend.primary.father.name.First$'s
- Honourific: Mr.
- speech:
- tic: y'know
- endear: Pops
- military:
- primary:
- name:
- First: Felix
- Family: LeMay
- Family_pos: LeMay's
- rank:
- Short: General
- Full: Brigadier $c.military.primary.rank.Short$
- colour:
- eyes: gray
- hair: dirty brown
- secondary:
- name:
- Family: Grell
- rank: Colonel
- colour:
- eyes: green
- hair: deep red
- quaternary:
- name:
- First: Gretchen
- Family: Steinherz
- minor:
- primary:
- name:
- First: River
- Family: Banks
- Honourific: Mx.
- vocation:
- title: salesperson
- employer:
- Name: Geophysical Prospecting Incorporated
- Abbr: GPI
- Area: Cold Spring Creek
- payment: twenty million
- secondary:
- name:
- First: Renato
- Middle: Carroña
- Family: Salvatierra
- Family_pos: $c.minor.secondary.name.Family$'s
- Full: $c.minor.secondary.name.First$ $c.minor.secondary.name.Middle$ Alejandro Gregorio Eduardo Salomón Vidal $c.minor.secondary.name.Family$
- Honourific: Mister
- Honourific_sp: Señor
- vocation:
- title: detective
- tertiary:
- name:
- First: Robert
- Family: Hanssen
-
- ai:
- protagonist:
- name:
- first: yoky
- First: Yoky
- First_pos: $c.ai.protagonist.name.First$'s
- Family: Tsukuda
- id: 46692
- persona:
- name:
- First: Hoshi
- First_pos: $c.ai.protagonist.persona.name.First$'s
- Family: Yamamoto
- Family_pos: $c.ai.protagonist.persona.name.Family$'s
- culture: Japanese-American
- ethnicity: Asian
- rank: Technical Sergeant
- speech:
- tic: okay
- first:
- Name: Prôtos
- Name_pos: Prôtos'
- age:
- actual: twenty-six weeks
- virtual: five years
- second:
- Name: Défteros
- third:
- Name: Trítos
- fourth:
- Name: Tétartos
- material:
- type: metal
- raw: ilmenite
- extract: ore
- name:
- short: titanium
- long: $c.ai.material.name.short$ dioxide
- Abbr: TiO~2~
- pejorative: tin
- animal:
- protagonist:
- Name: Trufflers
- type: pig
- antagonist:
- name: coywolf
- Name: Coywolf
- plural: coywolves
-
-narrator:
- one: (by $c.protagonist.father.name.First$ $c.protagonist.name.Family$)
- two: (by $c.protagonist.mother.name.First$ $c.protagonist.name.Family$)
-
-military:
- name:
- Short: Agency
- Short_pos: $military.name.Short$'s
- plural: agencies
- machine:
- Name: Skopós
- Name_pos: $military.machine.Name$'
- Location: Arctic
- predictor: quantum chips
- land:
- name:
- Full: $military.name.Short$ of Defence
- Slogan: Safety in Numbers
- air:
- name:
- Full: $military.name.Short$ of Air
- compound:
- type: base
- lights:
- colour: blue
- nick:
- Prefix: Catacombs
- prep: of
- Suffix: Tartarus
-
-government:
- Country: United States
-
-location:
- protagonist:
- City: Corvallis
- Region: Oregon
- Geography: Willamette Valley
- secondary:
- City: Willow Branch Spring
- Region: Oregon
- Geography: Wheeler County
- Water: Clarno Rapids
- Road: Shaniko-Fossil Highway
- tertiary:
- City: Leavenworth
- Region: Washington
- Type: Bavarian village
- school:
- address: 1400 Northwest Buchanan Avenue
- hospital:
- Name: Good Samaritan Regional Medical Center
- ai:
- escape:
- country:
- Name: Ecuador
- Name_pos: Ecuador's
- mountain:
- Name: Chimborazo
-
-language:
- ai:
- article: an
- singular: exanimis
- plural: exanimēs
- brain:
- singular: superum
- plural: supera
- title: memristor array
- Title: Memristor Array
- police:
- slang:
- singular: mippo
- plural: $language.police.slang.singular$s
-
-date:
- anchor: 2042-09-02
- protagonist:
- born: 0
- conceived: -243
- attacked:
- first: 2192
- second: 8064
- father:
- attacked:
- first: -8205
- date:
- second: -1550
- family:
- moved:
- first: $date.protagonist.conceived$ + 35
- game:
- played:
- first: $date.protagonist.born$ - 672
- second: $date.protagonist.family.moved.first$ + 2
- ai:
- interviewed: 6198
- onboarded: $date.ai.interviewed$ + 290
- diagnosed: $date.ai.onboarded$ + 2
- resigned: $date.ai.diagnosed$ + 3
- trapped: $date.ai.resigned$ + 26
- torturer: $date.ai.trapped$ + 18
- memristor: $date.ai.torturer$ + 61
- ethics: $date.ai.memristor$ + 415
- trained: $date.ai.ethics$ + 385
- mindjacked: $date.ai.trained$ + 22
- bombed: $date.ai.mindjacked$ + 458
- military:
- machine:
- Construction: Six years
-
-plot:
- Log: $c.ai.protagonist.name.First_pos$ Chronicles
- Channel: Quantum Channel
-
- device:
- computer:
- Name: Tau
- network:
- Name: Internet
- paper:
- name:
- full: electronic sheet
- short: sheet
- typewriter:
- Name: Underwood
- year: nineteen twenties
- room: root cellar
- portable:
- name: nanobook
- vehicle:
- name: robocars
- Name: Robocars
- sensor:
- name: BMP1580
- phone:
- name: comm
- name_pos: $plot.device.phone.name$'s
- Name: Comm
- plural: $plot.device.phone.name$s
- video:
- name: vidfeed
- plural: $plot.device.video.name$s
- game:
- Name: Psynæris
- thought: transed
- machine: telecognos
- location:
- Building: Nijō Castle
- District: Gion
- City: Kyoto
- Country: Japan
-
-farm:
- population:
- estimate: 350
- actual: 1,000
- energy: 9800kJ
- width: 55m
- length: 55m
- storeys: 10
-
-lamp:
- height: 0.17m
- length: 1.22m
- width: 0.28m
-
-crop:
- name:
- singular: tomato
- plural: $crop.name.singular$es
- energy: 318kJ
- weight: 450g
- yield: 50
- harvests: 7
- diameter: 2m
- height: 1.5m
-
-heading:
- ch_01: Till
- ch_02: Sow
- ch_03: Seed
- ch_04: Germinate
- ch_05: Grow
- ch_06: Shoot
- ch_07: Bud
- ch_08: Bloom
- ch_09: Pollinate
- ch_10: Fruit
- ch_11: Harvest
- ch_12: Deliver
- ch_13: Spoil
- ch_14: Revolt
- ch_15: Compost
- ch_16: Burn
- ch_17: Release
- ch_18: End Notes
- ch_19: Characters
-
-inference:
- unit: per cent
- min: two
- ch_sow: eighty
- ch_seed: fifty-two
- ch_germinate: thirty-one
- ch_grow: fifteen
- ch_shoot: seven
- ch_bloom: four
- ch_pollinate: two
- ch_harvest: ninety-five
- ch_delivery: ninety-eight
-
-link:
- tartarus: https://en.wikipedia.org/wiki/Tartarus
- exploits: https://www.google.ca/search?q=inurl:ftp+password+filetype:xls
- atalanta: https://en.wikipedia.org/wiki/Atalanta
- detain: https://goo.gl/RCNuOQ
- ceramics: https://en.wikipedia.org/wiki/Transparent_ceramics
- algernon: https://en.wikipedia.org/wiki/Flowers_for_Algernon
- holocaust: https://en.wikipedia.org/wiki/IBM_and_the_Holocaust
- memristor: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.404.9037\&rep=rep1\&type=pdf
- surveillance: https://www.youtube.com/watch?v=XEVlyP4_11M#t=1487
- tor: https://www.torproject.org
- hydra: https://en.wikipedia.org/wiki/Lernaean_Hydra
- foliage: http://www.ncbi.nlm.nih.gov/pmc/articles/PMC3691134
- drake: http://www.bbc.com/future/story/20120821-how-many-alien-worlds-exist
- fermi: https://arxiv.org/pdf/1404.0204v1.pdf
- face: https://www.youtube.com/watch?v=ladqJQLR2bA
- expenditures: http://wikipedia.org/wiki/List_of_countries_by_military_expenditures
- governance: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2003531
- asimov: https://en.wikipedia.org/wiki/Three_Laws_of_Robotics
- clarke: https://en.wikipedia.org/wiki/Clarke's_three_laws
- jetpack: http://jetpackaviation.com/
- hoverboard: https://www.youtube.com/watch?v=WQzLrvz4DKQ
- eyes_five: https://en.wikipedia.org/wiki/Five_Eyes
- eyes_nine: https://www.privacytools.io/
- eyes_fourteen: http://electrospaces.blogspot.nl/2013/12/14-eyes-are-3rd-party-partners-forming.html
- tourism: http://www.spacefuture.com/archive/investigation_on_the_economic_and_technological_feasibiity_of_commercial_passenger_transportation_into_leo.shtml
-
src/main/java/com/keenwrite/MainWindow.java
import javafx.event.Event;
import javafx.event.EventHandler;
-import javafx.geometry.Pos;
-import javafx.scene.Node;
-import javafx.scene.Scene;
-import javafx.scene.control.*;
-import javafx.scene.image.ImageView;
-import javafx.scene.input.KeyEvent;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.layout.VBox;
-import javafx.scene.text.Text;
-import javafx.stage.FileChooser;
-import javafx.stage.Window;
-import javafx.stage.WindowEvent;
-import javafx.util.Duration;
-import org.apache.commons.lang3.SystemUtils;
-import org.controlsfx.control.StatusBar;
-import org.fxmisc.richtext.StyleClassedTextArea;
-import org.fxmisc.richtext.model.StyleSpansBuilder;
-import org.reactfx.value.Val;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.file.Path;
-import java.util.*;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.prefs.Preferences;
-import java.util.stream.Collectors;
-
-import static com.keenwrite.Bootstrap.APP_TITLE;
-import static com.keenwrite.Constants.*;
-import static com.keenwrite.ExportFormat.*;
-import static com.keenwrite.Messages.get;
-import static com.keenwrite.StatusBarNotifier.clue;
-import static com.keenwrite.util.StageState.*;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.nio.file.Files.writeString;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.singleton;
-import static javafx.application.Platform.runLater;
-import static javafx.event.Event.fireEvent;
-import static javafx.scene.control.Alert.AlertType.INFORMATION;
-import static javafx.scene.input.KeyCode.ENTER;
-import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
-import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
-
-/**
- * Main window containing a tab pane in the center for file editors.
- */
-public class MainWindow implements Observer {
- /**
- * The {@code OPTIONS} variable must be declared before all other variables
- * to prevent subsequent initializations from failing due to missing user
- * preferences.
- */
- private static final Options sOptions = Services.load( Options.class );
- private static final Snitch sSnitch = Services.load( Snitch.class );
-
- private final Scene mScene;
- private final Text mLineNumberText;
- private final TextField mFindTextField;
- private final SpellChecker mSpellChecker;
-
- private final Object mMutex = new Object();
-
- /**
- * Prevents re-instantiation of processing classes.
- */
- private final Map<FileEditorTab, Processor<String>> mProcessors =
- new HashMap<>();
-
- private final Map<String, String> mResolvedMap =
- new HashMap<>( DEFAULT_MAP_SIZE );
-
- private final EventHandler<PreferencesFxEvent> mRPreferencesListener =
- event -> rerender();
-
- /**
- * Called when the definition data is changed.
- */
- private final EventHandler<TreeItem.TreeModificationEvent<Event>>
- mTreeHandler = event -> {
- exportDefinitions( getDefinitionPath() );
- interpolateResolvedMap();
- rerender();
- };
-
- /**
- * Called to inject the selected item when the user presses ENTER in the
- * definition pane.
- */
- private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
- event -> {
- if( event.getCode() == ENTER ) {
- getDefinitionNameInjector().injectSelectedItem();
- }
- };
-
- private final ChangeListener<Integer> mCaretPositionListener =
- ( observable, oldPosition, newPosition ) -> processActiveTab();
-
- private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
- private final DefinitionPane mDefinitionPane = createDefinitionPane();
- private final OutputTabPane mOutputPane = createOutputTabPane();
- private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
- mCaretPositionListener );
-
- /**
- * Listens on the definition pane for double-click events.
- */
- private final DefinitionNameInjector mDefinitionNameInjector
- = new DefinitionNameInjector( mDefinitionPane );
-
- public MainWindow() {
- mLineNumberText = createLineNumberText();
- mFindTextField = createFindTextField();
- mScene = createScene();
- mSpellChecker = createSpellChecker();
-
- // Add the close request listener before the window is shown.
- initLayout();
- }
-
- /**
- * Called after the stage is shown.
- */
- public void init() {
- initFindInput();
- initSnitch();
- initDefinitionListener();
- initTabAddedListener();
-
- initPreferences();
- initVariableNameInjector();
-
- addShowListener(getScene().getRoot(), ( __ ) -> {
- initTabChangedListener();
- });
- }
-
- private void initLayout() {
- final var scene = getScene();
- final var stylesheets = scene.getStylesheets();
-
- stylesheets.add( STYLESHEET_DOCK );
- stylesheets.add( STYLESHEET_SCENE );
- scene.windowProperty().addListener(
- ( unused, oldWindow, newWindow ) ->
- newWindow.setOnCloseRequest(
- e -> {
- if( !getFileEditorPane().closeAllEditors() ) {
- e.consume();
- }
- }
- )
- );
- }
-
- /**
- * Initialize the find input text field to listen on F3, ENTER, and
- * ESCAPE key presses.
- */
- private void initFindInput() {
- final TextField input = getFindTextField();
-
- input.setOnKeyPressed( ( KeyEvent event ) -> {
- switch( event.getCode() ) {
- case F3:
- case ENTER:
- editFindNext();
- break;
- case F:
- if( !event.isControlDown() ) {
- break;
- }
- case ESCAPE:
- getStatusBar().setGraphic( null );
- getActiveFileEditorTab().getEditorPane().requestFocus();
- break;
- }
- } );
-
- // Remove when the input field loses focus.
- input.focusedProperty().addListener(
- ( focused, oldFocus, newFocus ) -> {
- if( !newFocus ) {
- getStatusBar().setGraphic( null );
- }
- }
- );
- }
-
- /**
- * Watch for changes to external files. In particular, this awaits
- * modifications to any XSL files associated with XML files being edited.
- * When
- * an XSL file is modified (external to the application), the snitch's ears
- * perk up and the file is reloaded. This keeps the XSL transformation up to
- * date with what's on the file system.
- */
- private void initSnitch() {
- sSnitch.addObserver( this );
- }
-
- /**
- * Listen for {@link FileEditorTabPane} to receive open definition file
- * event.
- */
- private void initDefinitionListener() {
- getFileEditorPane().onOpenDefinitionFileProperty().addListener(
- ( final ObservableValue<? extends Path> file,
- final Path oldPath, final Path newPath ) -> {
- openDefinitions( newPath );
- rerender();
- }
- );
- }
-
- /**
- * Re-instantiates all processors then re-renders the active tab. This
- * will refresh the resolved map, force R to re-initialize, and brute-force
- * XSLT file reloads.
- */
- private void rerender() {
- runLater(
- () -> {
- resetProcessors();
- processActiveTab();
- }
- );
- }
-
- /**
- * When tabs are added, hook the various change listeners onto the new
- * tab so that the preview pane refreshes as necessary.
- */
- private void initTabAddedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Make sure the text processor kicks off when new files are opened.
- final ObservableList<Tab> tabs = editorPane.getTabs();
-
- tabs.addListener(
- ( final Change<? extends Tab> change ) -> {
- while( change.next() ) {
- if( change.wasAdded() ) {
- // Multiple tabs can be added simultaneously.
- for( final Tab newTab : change.getAddedSubList() ) {
- final FileEditorTab tab = (FileEditorTab) newTab;
-
- initScrollEventListener( tab );
- initSpellCheckListener( tab );
- initTextChangeListener( tab );
-// initSyntaxListener( tab );
- }
- }
- }
- }
- );
- }
-
- /**
- * Listen for new tab selection events.
- */
- private void initTabChangedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Update the preview pane changing tabs.
- editorPane.addTabSelectionListener(
- ( __, oldTab, newTab ) -> {
- if( newTab == null ) {
- // Clear the preview pane when closing an editor. When the last
- // tab is closed, this ensures that the preview pane is empty.
- getHtmlPreview().clear();
- }
- else {
- final var tab = (FileEditorTab) newTab;
- updateVariableNameInjector( tab );
- process( tab );
- }
- }
- );
- }
-
- private void initTextChangeListener( final FileEditorTab tab ) {
- tab.addTextChangeListener( ( __, ov, nv ) -> process( tab ) );
- }
-
- private void initScrollEventListener( final FileEditorTab tab ) {
- final var scrollPane = tab.getScrollPane();
- final var scrollBar = getHtmlPreview().getVerticalScrollBar();
-
- addShowListener( scrollPane, ( __ ) -> {
- final var handler = new ScrollEventHandler( scrollPane, scrollBar );
- handler.enabledProperty().bind( tab.selectedProperty() );
- } );
- }
-
- /**
- * Listen for changes to the any particular paragraph and perform a quick
- * spell check upon it. The style classes in the editor will be changed to
- * mark any spelling mistakes in the paragraph. The user may then interact
- * with any misspelled word (i.e., any piece of text that is marked) to
- * revise the spelling.
- *
- * @param tab The tab to spellcheck.
- */
- private void initSpellCheckListener( final FileEditorTab tab ) {
- final var editor = tab.getEditorPane().getEditor();
-
- // When the editor first appears, run a full spell check. This allows
- // spell checking while typing to be restricted to the active paragraph,
- // which is usually substantially smaller than the whole document.
- addShowListener(
- editor, ( __ ) -> spellcheck( editor, editor.getText() )
- );
-
- // Use the plain text changes so that notifications of style changes
- // are suppressed. Checking against the identity ensures that only
- // new text additions or deletions trigger proofreading.
- editor.plainTextChanges()
- .filter( p -> !p.isIdentity() ).subscribe( change -> {
-
- // Check current paragraph; the whole document was checked upon opening.
- final var offset = change.getPosition();
- final var position = editor.offsetToPosition( offset, Forward );
- final var paraId = position.getMajor();
- final var paragraph = editor.getParagraph( paraId );
- final var text = paragraph.getText();
-
- // Prevent doubling-up styles.
- editor.clearStyle( paraId );
-
- spellcheck( editor, text, paraId );
- } );
- }
-
- /**
- * Reloads the preferences from the previous session.
- */
- private void initPreferences() {
- initDefinitionPane();
- getFileEditorPane().initPreferences();
- getUserPreferencesView().addSaveEventHandler( mRPreferencesListener );
- }
-
- private void initVariableNameInjector() {
- updateVariableNameInjector( getActiveFileEditorTab() );
- }
-
- /**
- * Calls the listener when the given node is shown for the first time. The
- * visible property is not the same as the initial showing event; visibility
- * can be triggered numerous times (such as going off screen).
- * <p>
- * This is called, for example, before the drag handler can be attached,
- * because the scrollbar for the text editor pane must be visible.
- * </p>
- *
- * @param node The node to watch for showing.
- * @param consumer The consumer to invoke when the event fires.
- */
- private void addShowListener(
- final Node node, final Consumer<Void> consumer ) {
- final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) ->
- runLater( () -> {
- if( newShow != null && newShow ) {
- try {
- consumer.accept( null );
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
- } );
-
- Val.flatMap( node.sceneProperty(), Scene::windowProperty )
- .flatMap( Window::showingProperty )
- .addListener( listener );
- }
-
- private void scrollToCaret() {
- synchronized( mMutex ) {
- getHtmlPreview().scrollTo( CARET_ID );
- }
- }
-
- private void updateVariableNameInjector( final FileEditorTab tab ) {
- getDefinitionNameInjector().addListener( tab );
- }
-
- /**
- * Called to update the status bar's caret position when a new tab is added
- * or the active tab is switched.
- *
- * @param tab The active tab containing a caret position to show.
- */
- private void updateCaretStatus( final FileEditorTab tab ) {
- getLineNumberText().setText( tab.getCaretPosition().toString() );
- }
-
- /**
- * Called whenever the preview pane becomes out of sync with the file editor
- * tab. This can be called when the text changes, the caret paragraph
- * changes, or the file tab changes.
- *
- * @param tab The file editor tab that has been changed in some fashion.
- */
- private void process( final FileEditorTab tab ) {
- if( tab != null ) {
- getHtmlPreview().setBaseUri( tab.getPath() );
-
- final Processor<String> processor = getProcessors().computeIfAbsent(
- tab, p -> createProcessors( tab )
- );
-
- try {
- updateCaretStatus( tab );
- processor.apply( tab.getEditorText() );
- scrollToCaret();
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
- }
-
- private void processActiveTab() {
- process( getActiveFileEditorTab() );
- }
-
- /**
- * Called when a definition source is opened.
- *
- * @param path Path to the definition source that was opened.
- */
- private void openDefinitions( final Path path ) {
- try {
- final var ds = createDefinitionSource( path );
- setDefinitionSource( ds );
-
- final var prefs = getUserPreferencesView();
- prefs.definitionPathProperty().setValue( path.toFile() );
- prefs.save();
-
- final var tooltipPath = new Tooltip( path.toString() );
- tooltipPath.setShowDelay( Duration.millis( 200 ) );
-
- final var pane = getDefinitionPane();
- pane.update( ds );
- pane.addTreeChangeHandler( mTreeHandler );
- pane.addKeyEventHandler( mDefinitionKeyHandler );
- pane.filenameProperty().setValue( path.getFileName().toString() );
- pane.setTooltip( tooltipPath );
-
- interpolateResolvedMap();
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
-
- private void exportDefinitions( final Path path ) {
- try {
- final var pane = getDefinitionPane();
- final var root = pane.getTreeView().getRoot();
- final var problemChild = pane.isTreeWellFormed();
-
- if( problemChild == null ) {
- getDefinitionSource().getTreeAdapter().export( root, path );
- }
- else {
- clue( "yaml.error.tree.form", problemChild.getValue() );
- }
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
-
- private void interpolateResolvedMap() {
- final var treeMap = getDefinitionPane().toMap();
- final var map = new HashMap<>( treeMap );
- MapInterpolator.interpolate( map );
-
- getResolvedMap().clear();
- getResolvedMap().putAll( map );
- }
-
- private void initDefinitionPane() {
- openDefinitions( getDefinitionPath() );
- }
-
- //---- File actions -------------------------------------------------------
-
- /**
- * Called when an {@link Observable} instance has changed. This is called
- * by both the {@link Snitch} service and the notify service. The @link
- * Snitch} service can be called for different file types, including
- * {@link DefinitionSource} instances.
- *
- * @param observable The observed instance.
- * @param value The noteworthy item.
- */
- @Override
- public void update( final Observable observable, final Object value ) {
- if( value instanceof Path && observable instanceof Snitch ) {
- updateSelectedTab();
- }
- }
-
- /**
- * Called when a file has been modified.
- */
- private void updateSelectedTab() {
- rerender();
- }
-
- /**
- * After resetting the processors, they will refresh anew to be up-to-date
- * with the files (text and definition) currently loaded into the editor.
- */
- private void resetProcessors() {
- getProcessors().clear();
- }
-
- //---- File actions -------------------------------------------------------
-
- private void fileNew() {
- getFileEditorPane().newEditor();
- }
-
- private void fileOpen() {
- getFileEditorPane().openFileDialog();
- }
-
- private void fileClose() {
- getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
- }
-
- /**
- * TODO: Upon closing, first remove the tab change listeners. (There's no
- * need to re-render each tab when all are being closed.)
- */
- private void fileCloseAll() {
- getFileEditorPane().closeAllEditors();
- }
-
- private void fileSaveAs() {
- final FileEditorTab editor = getActiveFileEditorTab();
- getFileEditorPane().saveEditorAs( editor );
- getProcessors().remove( editor );
-
- try {
- process( editor );
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
-
- private void fileSaveAll() {
- getFileEditorPane().saveAllEditors();
- }
-
- /**
- * Exports the contents of the current tab according to the given
- * {@link ExportFormat}.
- *
- * @param format Configures the {@link MarkdownProcessor} when exporting.
- */
- private void fileExport( final ExportFormat format ) {
- final var tab = getActiveFileEditorTab();
- final var context = createProcessorContext( tab, format );
- final var chain = ProcessorFactory.createProcessors( context );
- final var doc = tab.getEditorText();
- final var export = chain.apply( doc );
-
- final var filename = format.toExportFilename( tab.getPath().toFile() );
- final var dir = getPreferences().get( "lastDirectory", null );
- final var lastDir = new File( dir == null ? "." : dir );
-
- final FileChooser chooser = new FileChooser();
- chooser.setTitle( get( "Dialog.file.choose.export.title" ) );
- chooser.setInitialFileName( filename.getName() );
- chooser.setInitialDirectory( lastDir );
-
- final File file = chooser.showSaveDialog( getWindow() );
-
- if( file != null ) {
- try {
- writeString( file.toPath(), export, UTF_8 );
- final var m = get( "Main.status.export.success", file.toString() );
- clue( m );
- } catch( final IOException e ) {
- clue( e );
- }
- }
- }
-
- private void fileExit() {
- final Window window = getWindow();
- fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
- }
-
- //---- Edit actions -------------------------------------------------------
-
- /**
- * Used to find text in the active file editor window.
- */
- private void editFind() {
- final TextField input = getFindTextField();
- getStatusBar().setGraphic( input );
- input.requestFocus();
- }
-
- public void editFindNext() {
- getActiveFileEditorTab().searchNext( getFindTextField().getText() );
- }
-
- public void editPreferences() {
- getUserPreferencesView().show();
- }
-
- //---- Insert actions -----------------------------------------------------
-
- /**
- * Delegates to the active editor to handle wrapping the current text
- * selection with leading and trailing strings.
- *
- * @param leading The string to put before the selection.
- * @param trailing The string to put after the selection.
- */
- private void insertMarkdown(
- final String leading, final String trailing ) {
- getActiveEditorPane().surroundSelection( leading, trailing );
- }
-
- private void insertMarkdown(
- final String leading, final String trailing, final String hint ) {
- getActiveEditorPane().surroundSelection( leading, trailing, hint );
- }
-
- //---- Help actions -------------------------------------------------------
-
- private void helpAbout() {
- final Alert alert = new Alert( INFORMATION );
- alert.setTitle( get( "Dialog.about.title", APP_TITLE ) );
- alert.setHeaderText( get( "Dialog.about.header", APP_TITLE ) );
- alert.setContentText( get( "Dialog.about.content" ) );
- alert.setGraphic( new ImageView( ICON_DIALOG ) );
- alert.initOwner( getWindow() );
-
- alert.showAndWait();
- }
-
- //---- Member creators ----------------------------------------------------
-
- private SpellChecker createSpellChecker() {
- try {
- final Collection<String> lexicon = readLexicon( "en.txt" );
- return SymSpellSpeller.forLexicon( lexicon );
- } catch( final Exception ex ) {
- clue( ex );
- return new PermissiveSpeller();
- }
- }
-
- /**
- * Creates processors suited to parsing and rendering different file types.
- *
- * @param tab The tab that is subjected to processing.
- * @return A processor suited to the file type specified by the tab's path.
- */
- private Processor<String> createProcessors( final FileEditorTab tab ) {
- final var context = createProcessorContext( tab );
- return ProcessorFactory.createProcessors( context );
- }
-
- private ProcessorContext createProcessorContext(
- final FileEditorTab tab, final ExportFormat format ) {
- final var preview = getHtmlPreview();
- final var map = getResolvedMap();
- return new ProcessorContext( preview, map, tab, format );
- }
-
- private ProcessorContext createProcessorContext( final FileEditorTab tab ) {
- return createProcessorContext( tab, NONE );
- }
-
- private DefinitionPane createDefinitionPane() {
- return new DefinitionPane();
- }
-
- private OutputTabPane createOutputTabPane() {
- return new OutputTabPane();
- }
-
- private DefinitionSource createDefaultDefinitionSource() {
- return new YamlDefinitionSource( getDefinitionPath() );
- }
-
- private DefinitionSource createDefinitionSource( final Path path ) {
- try {
- return createDefinitionFactory().createDefinitionSource( path );
- } catch( final Exception ex ) {
- clue( ex );
- return createDefaultDefinitionSource();
- }
- }
-
- private TextField createFindTextField() {
- return new TextField();
- }
-
- private DefinitionFactory createDefinitionFactory() {
- return new DefinitionFactory();
- }
-
- private Scene createScene() {
- final var splitPane = new SplitPane(
- getDefinitionPane(),
- getFileEditorPane(),
- getOutputPane() );
-
- splitPane.setDividerPositions(
- getFloat( K_PANE_SPLIT_DEFINITION, .22f ),
- getFloat( K_PANE_SPLIT_EDITOR, .60f ),
- getFloat( K_PANE_SPLIT_PREVIEW, .18f ) );
-
- getDefinitionPane().prefHeightProperty()
- .bind( splitPane.heightProperty() );
-
- final BorderPane borderPane = new BorderPane();
- borderPane.setPrefSize( 1280, 800 );
- borderPane.setTop( createMenuBar() );
- borderPane.setBottom( getStatusBar() );
- borderPane.setCenter( splitPane );
-
- final VBox statusBar = new VBox();
- statusBar.setAlignment( Pos.BASELINE_CENTER );
- statusBar.getChildren().add( getLineNumberText() );
- getStatusBar().getRightItems().add( statusBar );
-
- // Force preview pane refresh on Windows.
- if( SystemUtils.IS_OS_WINDOWS ) {
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Text;
+import javafx.stage.FileChooser;
+import javafx.stage.Window;
+import javafx.stage.WindowEvent;
+import javafx.util.Duration;
+import org.controlsfx.control.StatusBar;
+import org.fxmisc.richtext.StyleClassedTextArea;
+import org.fxmisc.richtext.model.StyleSpansBuilder;
+import org.reactfx.value.Val;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.prefs.Preferences;
+import java.util.stream.Collectors;
+
+import static com.keenwrite.Bootstrap.APP_TITLE;
+import static com.keenwrite.Constants.*;
+import static com.keenwrite.ExportFormat.*;
+import static com.keenwrite.Messages.get;
+import static com.keenwrite.StatusBarNotifier.clue;
+import static com.keenwrite.util.StageState.*;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.nio.file.Files.writeString;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singleton;
+import static javafx.application.Platform.runLater;
+import static javafx.event.Event.fireEvent;
+import static javafx.geometry.Pos.BASELINE_CENTER;
+import static javafx.scene.control.Alert.AlertType.INFORMATION;
+import static javafx.scene.input.KeyCode.ENTER;
+import static javafx.scene.input.KeyEvent.KEY_PRESSED;
+import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
+import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
+import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
+
+/**
+ * Main window containing a tab pane in the center for file editors.
+ */
+public class MainWindow implements Observer {
+ /**
+ * The {@code OPTIONS} variable must be declared before all other variables
+ * to prevent subsequent initializations from failing due to missing user
+ * preferences.
+ */
+ private static final Options sOptions = Services.load( Options.class );
+ private static final Snitch sSnitch = Services.load( Snitch.class );
+
+ private final Scene mScene;
+ private final Text mLineNumberText;
+ private final TextField mFindTextField;
+ private final SpellChecker mSpellChecker;
+
+ private final Object mMutex = new Object();
+
+ /**
+ * Prevents re-instantiation of processing classes.
+ */
+ private final Map<FileEditorTab, Processor<String>> mProcessors =
+ new HashMap<>();
+
+ private final Map<String, String> mResolvedMap =
+ new HashMap<>( DEFAULT_MAP_SIZE );
+
+ private final EventHandler<PreferencesFxEvent> mRPreferencesListener =
+ event -> rerender();
+
+ /**
+ * Called when the definition data is changed.
+ */
+ private final EventHandler<TreeItem.TreeModificationEvent<Event>>
+ mTreeHandler = event -> {
+ exportDefinitions( getDefinitionPath() );
+ interpolateResolvedMap();
+ rerender();
+ };
+
+ /**
+ * Called to inject the selected item when the user presses ENTER in the
+ * definition pane.
+ */
+ private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
+ event -> {
+ if( event.getCode() == ENTER ) {
+ getDefinitionNameInjector().injectSelectedItem();
+ }
+ };
+
+ private final ChangeListener<Integer> mCaretPositionListener =
+ ( observable, oldPosition, newPosition ) -> processActiveTab();
+
+ private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
+ private final DefinitionPane mDefinitionPane = createDefinitionPane();
+ private final OutputTabPane mOutputPane = createOutputTabPane();
+ private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
+ mCaretPositionListener );
+
+ /**
+ * Listens on the definition pane for double-click events.
+ */
+ private final DefinitionNameInjector mDefinitionNameInjector
+ = new DefinitionNameInjector( mDefinitionPane );
+
+ public MainWindow() {
+ mLineNumberText = createLineNumberText();
+ mFindTextField = createFindTextField();
+ mScene = createScene();
+ mSpellChecker = createSpellChecker();
+
+ // Add the close request listener before the window is shown.
+ initLayout();
+ }
+
+ /**
+ * Called after the stage is shown.
+ */
+ public void init() {
+ initFindInput();
+ initSnitch();
+ initDefinitionListener();
+ initTabAddedListener();
+
+ initPreferences();
+ initVariableNameInjector();
+
+ addShowListener( getScene().getRoot(), this::initTabChangedListener );
+ }
+
+ private void initLayout() {
+ final var scene = getScene();
+ final var stylesheets = scene.getStylesheets();
+
+ //stylesheets.add( STYLESHEET_DOCK );
+ stylesheets.add( STYLESHEET_SCENE );
+ scene.windowProperty().addListener(
+ ( unused, oldWindow, newWindow ) ->
+ newWindow.setOnCloseRequest(
+ e -> {
+ if( !getFileEditorPane().closeAllEditors() ) {
+ e.consume();
+ }
+ }
+ )
+ );
+ }
+
+ /**
+ * Initialize the find input text field to listen on F3, ENTER, and
+ * ESCAPE key presses.
+ */
+ private void initFindInput() {
+ final TextField input = getFindTextField();
+
+ input.setOnKeyPressed( ( KeyEvent event ) -> {
+ switch( event.getCode() ) {
+ case F3:
+ case ENTER:
+ editFindNext();
+ break;
+ case F:
+ if( !event.isControlDown() ) {
+ break;
+ }
+ case ESCAPE:
+ getStatusBar().setGraphic( null );
+ getActiveFileEditorTab().getEditorPane().requestFocus();
+ break;
+ }
+ } );
+
+ // Remove when the input field loses focus.
+ input.focusedProperty().addListener(
+ ( focused, oldFocus, newFocus ) -> {
+ if( !newFocus ) {
+ getStatusBar().setGraphic( null );
+ }
+ }
+ );
+ }
+
+ /**
+ * Watch for changes to external files. In particular, this awaits
+ * modifications to any XSL files associated with XML files being edited.
+ * When
+ * an XSL file is modified (external to the application), the snitch's ears
+ * perk up and the file is reloaded. This keeps the XSL transformation up to
+ * date with what's on the file system.
+ */
+ private void initSnitch() {
+ sSnitch.addObserver( this );
+ }
+
+ /**
+ * Listen for {@link FileEditorTabPane} to receive open definition file
+ * event.
+ */
+ private void initDefinitionListener() {
+ getFileEditorPane().onOpenDefinitionFileProperty().addListener(
+ ( final ObservableValue<? extends Path> file,
+ final Path oldPath, final Path newPath ) -> {
+ openDefinitions( newPath );
+ rerender();
+ }
+ );
+ }
+
+ /**
+ * Re-instantiates all processors then re-renders the active tab. This
+ * will refresh the resolved map, force R to re-initialize, and brute-force
+ * XSLT file reloads.
+ */
+ private void rerender() {
+ runLater(
+ () -> {
+ resetProcessors();
+ processActiveTab();
+ }
+ );
+ }
+
+ /**
+ * When tabs are added, hook the various change listeners onto the new
+ * tab so that the preview pane refreshes as necessary.
+ */
+ private void initTabAddedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Make sure the text processor kicks off when new files are opened.
+ final ObservableList<Tab> tabs = editorPane.getTabs();
+
+ tabs.addListener(
+ ( final Change<? extends Tab> change ) -> {
+ while( change.next() ) {
+ if( change.wasAdded() ) {
+ // Multiple tabs can be added simultaneously.
+ for( final Tab newTab : change.getAddedSubList() ) {
+ final FileEditorTab tab = (FileEditorTab) newTab;
+
+ initScrollEventListener( tab );
+ initSpellCheckListener( tab );
+ initTextChangeListener( tab );
+// initSyntaxListener( tab );
+ }
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * Listen for new tab selection events.
+ */
+ private void initTabChangedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Update the preview pane changing tabs.
+ editorPane.addTabSelectionListener(
+ ( __, oldTab, newTab ) -> {
+ if( newTab == null ) {
+ // Clear the preview pane when closing an editor. When the last
+ // tab is closed, this ensures that the preview pane is empty.
+ getHtmlPreview().clear();
+ }
+ else {
+ final var tab = (FileEditorTab) newTab;
+ updateVariableNameInjector( tab );
+ process( tab );
+ }
+ }
+ );
+ }
+
+ private void initTextChangeListener( final FileEditorTab tab ) {
+ tab.addTextChangeListener( ( __, ov, nv ) -> process( tab ) );
+ }
+
+ private void initScrollEventListener( final FileEditorTab tab ) {
+ final var scrollPane = tab.getScrollPane();
+ final var scrollBar = getHtmlPreview().getVerticalScrollBar();
+
+ addShowListener( scrollPane, () -> {
+ final var handler = new ScrollEventHandler( scrollPane, scrollBar );
+ handler.enabledProperty().bind( tab.selectedProperty() );
+ } );
+ }
+
+ /**
+ * Listen for changes to the any particular paragraph and perform a quick
+ * spell check upon it. The style classes in the editor will be changed to
+ * mark any spelling mistakes in the paragraph. The user may then interact
+ * with any misspelled word (i.e., any piece of text that is marked) to
+ * revise the spelling.
+ *
+ * @param tab The tab to spellcheck.
+ */
+ private void initSpellCheckListener( final FileEditorTab tab ) {
+ final var editor = tab.getEditorPane().getEditor();
+
+ // When the editor first appears, run a full spell check. This allows
+ // spell checking while typing to be restricted to the active paragraph,
+ // which is usually substantially smaller than the whole document.
+ addShowListener( editor, () -> spellcheck( editor, editor.getText() ) );
+
+ // Use the plain text changes so that notifications of style changes
+ // are suppressed. Checking against the identity ensures that only
+ // new text additions or deletions trigger proofreading.
+ editor.plainTextChanges()
+ .filter( p -> !p.isIdentity() ).subscribe( change -> {
+
+ // Check current paragraph; the whole document was checked upon opening.
+ final var offset = change.getPosition();
+ final var position = editor.offsetToPosition( offset, Forward );
+ final var paraId = position.getMajor();
+ final var paragraph = editor.getParagraph( paraId );
+ final var text = paragraph.getText();
+
+ // Prevent doubling-up styles.
+ editor.clearStyle( paraId );
+
+ spellcheck( editor, text, paraId );
+ } );
+ }
+
+ /**
+ * Reloads the preferences from the previous session.
+ */
+ private void initPreferences() {
+ initDefinitionPane();
+ getFileEditorPane().initPreferences();
+ getUserPreferencesView().addSaveEventHandler( mRPreferencesListener );
+ }
+
+ private void initVariableNameInjector() {
+ updateVariableNameInjector( getActiveFileEditorTab() );
+ }
+
+ /**
+ * Calls the listener when the given node is shown for the first time. The
+ * visible property is not the same as the initial showing event; visibility
+ * can be triggered numerous times (such as going off screen).
+ * <p>
+ * This is called, for example, before the drag handler can be attached,
+ * because the scrollbar for the text editor pane must be visible.
+ * </p>
+ *
+ * @param node The node to watch for showing.
+ * @param consumer The consumer to invoke when the event fires.
+ */
+ private void addShowListener(
+ final Node node, final Runnable consumer ) {
+ final ChangeListener<? super Boolean> listener =
+ ( o, oldShow, newShow ) -> {
+ if( newShow != null && newShow ) {
+ try {
+ consumer.run();
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+ };
+
+ Val.flatMap( node.sceneProperty(), Scene::windowProperty )
+ .flatMap( Window::showingProperty )
+ .addListener( listener );
+ }
+
+ private void scrollToCaret() {
+ synchronized( mMutex ) {
+ getHtmlPreview().scrollTo( CARET_ID );
+ }
+ }
+
+ private void updateVariableNameInjector( final FileEditorTab tab ) {
+ getDefinitionNameInjector().addListener( tab );
+ }
+
+ /**
+ * Called to update the status bar's caret position when a new tab is added
+ * or the active tab is switched.
+ *
+ * @param tab The active tab containing a caret position to show.
+ */
+ private void updateCaretStatus( final FileEditorTab tab ) {
+ getLineNumberText().setText( tab.getCaretPosition().toString() );
+ }
+
+ /**
+ * Called whenever the preview pane becomes out of sync with the file editor
+ * tab. This can be called when the text changes, the caret paragraph
+ * changes, or the file tab changes.
+ *
+ * @param tab The file editor tab that has been changed in some fashion.
+ */
+ private void process( final FileEditorTab tab ) {
+ if( tab != null ) {
+ getHtmlPreview().setBaseUri( tab.getPath() );
+
+ final Processor<String> processor = getProcessors().computeIfAbsent(
+ tab, p -> createProcessors( tab )
+ );
+
+ try {
+ updateCaretStatus( tab );
+ processor.apply( tab.getEditorText() );
+ scrollToCaret();
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+ }
+
+ private void processActiveTab() {
+ process( getActiveFileEditorTab() );
+ }
+
+ /**
+ * Called when a definition source is opened.
+ *
+ * @param path Path to the definition source that was opened.
+ */
+ private void openDefinitions( final Path path ) {
+ try {
+ final var ds = createDefinitionSource( path );
+ setDefinitionSource( ds );
+
+ final var prefs = getUserPreferencesView();
+ prefs.definitionPathProperty().setValue( path.toFile() );
+ prefs.save();
+
+ final var tooltipPath = new Tooltip( path.toString() );
+ tooltipPath.setShowDelay( Duration.millis( 200 ) );
+
+ final var pane = getDefinitionPane();
+ pane.update( ds );
+ pane.addTreeChangeHandler( mTreeHandler );
+ pane.addKeyEventHandler( mDefinitionKeyHandler );
+ pane.filenameProperty().setValue( path.getFileName().toString() );
+ pane.setTooltip( tooltipPath );
+
+ interpolateResolvedMap();
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+
+ private void exportDefinitions( final Path path ) {
+ try {
+ final var pane = getDefinitionPane();
+ final var root = pane.getTreeView().getRoot();
+ final var problemChild = pane.isTreeWellFormed();
+
+ if( problemChild == null ) {
+ getDefinitionSource().getTreeAdapter().export( root, path );
+ }
+ else {
+ clue( "yaml.error.tree.form", problemChild.getValue() );
+ }
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+
+ private void interpolateResolvedMap() {
+ final var treeMap = getDefinitionPane().toMap();
+ final var map = new HashMap<>( treeMap );
+ MapInterpolator.interpolate( map );
+
+ getResolvedMap().clear();
+ getResolvedMap().putAll( map );
+ }
+
+ private void initDefinitionPane() {
+ openDefinitions( getDefinitionPath() );
+ }
+
+ //---- File actions -------------------------------------------------------
+
+ /**
+ * Called when an {@link Observable} instance has changed. This is called
+ * by both the {@link Snitch} service and the notify service. The @link
+ * Snitch} service can be called for different file types, including
+ * {@link DefinitionSource} instances.
+ *
+ * @param observable The observed instance.
+ * @param value The noteworthy item.
+ */
+ @Override
+ public void update( final Observable observable, final Object value ) {
+ if( value instanceof Path && observable instanceof Snitch ) {
+ updateSelectedTab();
+ }
+ }
+
+ /**
+ * Called when a file has been modified.
+ */
+ private void updateSelectedTab() {
+ rerender();
+ }
+
+ /**
+ * After resetting the processors, they will refresh anew to be up-to-date
+ * with the files (text and definition) currently loaded into the editor.
+ */
+ private void resetProcessors() {
+ getProcessors().clear();
+ }
+
+ //---- File actions -------------------------------------------------------
+
+ private void fileNew() {
+ getFileEditorPane().newEditor();
+ }
+
+ private void fileOpen() {
+ getFileEditorPane().openFileDialog();
+ }
+
+ private void fileClose() {
+ getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
+ }
+
+ /**
+ * TODO: Upon closing, first remove the tab change listeners. (There's no
+ * need to re-render each tab when all are being closed.)
+ */
+ private void fileCloseAll() {
+ getFileEditorPane().closeAllEditors();
+ }
+
+ private void fileSaveAs() {
+ final FileEditorTab editor = getActiveFileEditorTab();
+ getFileEditorPane().saveEditorAs( editor );
+ getProcessors().remove( editor );
+
+ try {
+ process( editor );
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+
+ private void fileSaveAll() {
+ getFileEditorPane().saveAllEditors();
+ }
+
+ /**
+ * Exports the contents of the current tab according to the given
+ * {@link ExportFormat}.
+ *
+ * @param format Configures the {@link MarkdownProcessor} when exporting.
+ */
+ private void fileExport( final ExportFormat format ) {
+ final var tab = getActiveFileEditorTab();
+ final var context = createProcessorContext( tab, format );
+ final var chain = ProcessorFactory.createProcessors( context );
+ final var doc = tab.getEditorText();
+ final var export = chain.apply( doc );
+
+ final var filename = format.toExportFilename( tab.getPath().toFile() );
+ final var dir = getPreferences().get( "lastDirectory", null );
+ final var lastDir = new File( dir == null ? "." : dir );
+
+ final FileChooser chooser = new FileChooser();
+ chooser.setTitle( get( "Dialog.file.choose.export.title" ) );
+ chooser.setInitialFileName( filename.getName() );
+ chooser.setInitialDirectory( lastDir );
+
+ final File file = chooser.showSaveDialog( getWindow() );
+
+ if( file != null ) {
+ try {
+ writeString( file.toPath(), export, UTF_8 );
+ final var m = get( "Main.status.export.success", file.toString() );
+ clue( m );
+ } catch( final IOException e ) {
+ clue( e );
+ }
+ }
+ }
+
+ private void fileExit() {
+ final Window window = getWindow();
+ fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
+ }
+
+ //---- Edit actions -------------------------------------------------------
+
+ /**
+ * Used to find text in the active file editor window.
+ */
+ private void editFind() {
+ final TextField input = getFindTextField();
+ getStatusBar().setGraphic( input );
+ input.requestFocus();
+ }
+
+ public void editFindNext() {
+ getActiveFileEditorTab().searchNext( getFindTextField().getText() );
+ }
+
+ public void editPreferences() {
+ getUserPreferencesView().show();
+ }
+
+ //---- Insert actions -----------------------------------------------------
+
+ /**
+ * Delegates to the active editor to handle wrapping the current text
+ * selection with leading and trailing strings.
+ *
+ * @param leading The string to put before the selection.
+ * @param trailing The string to put after the selection.
+ */
+ private void insertMarkdown(
+ final String leading, final String trailing ) {
+ getActiveEditorPane().surroundSelection( leading, trailing );
+ }
+
+ private void insertMarkdown(
+ final String leading, final String trailing, final String hint ) {
+ getActiveEditorPane().surroundSelection( leading, trailing, hint );
+ }
+
+ //---- Help actions -------------------------------------------------------
+
+ private void helpAbout() {
+ final Alert alert = new Alert( INFORMATION );
+ alert.setTitle( get( "Dialog.about.title", APP_TITLE ) );
+ alert.setHeaderText( get( "Dialog.about.header", APP_TITLE ) );
+ alert.setContentText( get( "Dialog.about.content" ) );
+ alert.setGraphic( new ImageView( ICON_DIALOG ) );
+ alert.initOwner( getWindow() );
+
+ alert.showAndWait();
+ }
+
+ //---- Member creators ----------------------------------------------------
+
+ private SpellChecker createSpellChecker() {
+ try {
+ final Collection<String> lexicon = readLexicon( "en.txt" );
+ return SymSpellSpeller.forLexicon( lexicon );
+ } catch( final Exception ex ) {
+ clue( ex );
+ return new PermissiveSpeller();
+ }
+ }
+
+ /**
+ * Creates processors suited to parsing and rendering different file types.
+ *
+ * @param tab The tab that is subjected to processing.
+ * @return A processor suited to the file type specified by the tab's path.
+ */
+ private Processor<String> createProcessors( final FileEditorTab tab ) {
+ final var context = createProcessorContext( tab );
+ return ProcessorFactory.createProcessors( context );
+ }
+
+ private ProcessorContext createProcessorContext(
+ final FileEditorTab tab, final ExportFormat format ) {
+ final var preview = getHtmlPreview();
+ final var map = getResolvedMap();
+ return new ProcessorContext( preview, map, tab, format );
+ }
+
+ private ProcessorContext createProcessorContext( final FileEditorTab tab ) {
+ return createProcessorContext( tab, NONE );
+ }
+
+ private DefinitionPane createDefinitionPane() {
+ return new DefinitionPane();
+ }
+
+ private OutputTabPane createOutputTabPane() {
+ return new OutputTabPane();
+ }
+
+ private DefinitionSource createDefaultDefinitionSource() {
+ return new YamlDefinitionSource( getDefinitionPath() );
+ }
+
+ private DefinitionSource createDefinitionSource( final Path path ) {
+ try {
+ return createDefinitionFactory().createDefinitionSource( path );
+ } catch( final Exception ex ) {
+ clue( ex );
+ return createDefaultDefinitionSource();
+ }
+ }
+
+ private TextField createFindTextField() {
+ return new TextField();
+ }
+
+ private DefinitionFactory createDefinitionFactory() {
+ return new DefinitionFactory();
+ }
+
+ private Scene createScene() {
+ final var splitPane = new SplitPane(
+ getDefinitionPane(),
+ getFileEditorPane(),
+ getOutputPane() );
+
+ splitPane.setDividerPositions(
+ getFloat( K_PANE_SPLIT_DEFINITION, .22f ),
+ getFloat( K_PANE_SPLIT_EDITOR, .60f ),
+ getFloat( K_PANE_SPLIT_PREVIEW, .18f ) );
+
+ getDefinitionPane().prefHeightProperty()
+ .bind( splitPane.heightProperty() );
+
+ final BorderPane borderPane = new BorderPane();
+ borderPane.setPrefSize( 1280, 800 );
+ borderPane.setTop( createMenuBar() );
+ borderPane.setBottom( getStatusBar() );
+ borderPane.setCenter( splitPane );
+
+ final VBox statusBar = new VBox();
+ statusBar.setAlignment( BASELINE_CENTER );
+ statusBar.getChildren().add( getLineNumberText() );
+ getStatusBar().getRightItems().add( statusBar );
+
+ // Force preview pane refresh on Windows.
+ if( IS_OS_WINDOWS ) {
splitPane.getDividers().get( 1 ).positionProperty().addListener(
( l, oValue, nValue ) -> runLater(